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.
Passed
Push — master ( b8a07f...92fd45 )
by Anton
03:55
created

appendToFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
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\Support\ObjectProxy;
23
use Deployer\Task\Context;
24
use Deployer\Task\GroupTask;
25
use Deployer\Task\Task;
26
use Deployer\Utility\Httpie;
27
use Symfony\Component\Console\Helper\QuestionHelper;
28
use Symfony\Component\Console\Input\InputInterface;
29
use Symfony\Component\Console\Input\InputOption;
30
use Symfony\Component\Console\Output\OutputInterface;
31
use Symfony\Component\Console\Question\ChoiceQuestion;
32
use Symfony\Component\Console\Question\ConfirmationQuestion;
33
use Symfony\Component\Console\Question\Question;
34
35
use function Deployer\Support\array_merge_alternate;
36
use function Deployer\Support\env_stringify;
37
use function Deployer\Support\escape_shell_argument;
38
use function Deployer\Support\is_closure;
39
use function Deployer\Support\str_contains;
40 1
41 1
/**
42
 * Defines a host or hosts.
43 1
 * ```php
44 1
 * host('example.org');
45
 * host('prod.example.org', 'staging.example.org');
46
 * ```
47
 *
48
 * Inside task can be used to get `Host` instance of an alias.
49
 * ```php
50
 * task('test', function () {
51
 *     $port = host('example.org')->get('port');
52
 * });
53
 * ```
54
 *
55
 * @return Host|ObjectProxy
56 1
 */
57 1
function host(string ...$hostname)
58 1
{
59 1
    $deployer = Deployer::get();
60
    if (count($hostname) === 1 && $deployer->hosts->has($hostname[0])) {
61
        return $deployer->hosts->get($hostname[0]);
62 1
    }
63 1
    $aliases = Range::expand($hostname);
64 1
65 1
    foreach ($aliases as $alias) {
66 1
        if ($deployer->hosts->has($alias)) {
67
            $host = $deployer->hosts->get($alias);
68
            throw new \InvalidArgumentException("Host \"$host\" already exists.");
69
        }
70
    }
71
72
    if (count($aliases) === 1) {
73
        $host = new Host($aliases[0]);
74
        $deployer->hosts->set($aliases[0], $host);
75
        return $host;
76 13
    } else {
77 13
        $hosts = array_map(function ($hostname) use ($deployer): Host {
78
            $host = new Host($hostname);
79 13
            $deployer->hosts->set($hostname, $host);
80 13
            return $host;
81 13
        }, $aliases);
82 13
        return new ObjectProxy($hosts);
83
    }
84
}
85
86
/**
87
 * @return Localhost|ObjectProxy
88
 */
89
function localhost(string ...$hostnames)
90
{
91
    $deployer = Deployer::get();
92
    $hostnames = Range::expand($hostnames);
93
94
    if (count($hostnames) <= 1) {
95
        $host = count($hostnames) === 1 ? new Localhost($hostnames[0]) : new Localhost();
96
        $deployer->hosts->set($host->getAlias(), $host);
0 ignored issues
show
Bug introduced by
It seems like $host->getAlias() can also be of type null; however, parameter $name of Deployer\Collection\Collection::set() 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

96
        $deployer->hosts->set(/** @scrutinizer ignore-type */ $host->getAlias(), $host);
Loading history...
97
        return $host;
98
    } else {
99
        $hosts = array_map(function ($hostname) use ($deployer): Localhost {
100
            $host = new Localhost($hostname);
101 5
            $deployer->hosts->set($host->getAlias(), $host);
0 ignored issues
show
Bug introduced by
It seems like $host->getAlias() can also be of type null; however, parameter $name of Deployer\Collection\Collection::set() 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

101
            $deployer->hosts->set(/** @scrutinizer ignore-type */ $host->getAlias(), $host);
Loading history...
102
            return $host;
103
        }, $hostnames);
104
        return new ObjectProxy($hosts);
105
    }
106
}
107
108
/**
109
 * Returns current host.
110
 */
111 8
function currentHost(): Host
112
{
113
    return Context::get()->getHost();
114
}
115
116
/**
117
 * Returns hosts based on provided selector.
118
 *
119
 * ```php
120
 * on(select('stage=prod, role=db'), function (Host $host) {
121
 *     ...
122
 * });
123
 * ```
124
 *
125
 * @return Host[]
126
 */
127
function select(string $selector): array
128
{
129
    return Deployer::get()->selector->select($selector);
130
}
131
132
/**
133
 * Returns array of hosts selected by user via CLI.
134
 *
135
 * @return Host[]
136
 */
137
function selectedHosts(): array
138
{
139
    $hosts = [];
140
    foreach (get('selected_hosts', []) as $alias) {
141
        $hosts[] = Deployer::get()->hosts->get($alias);
142
    }
143 15
    return $hosts;
144
}
145 15
146 15
/**
147
 * Import other php or yaml recipes.
148 8
 *
149
 * ```php
150
 * import('recipe/common.php');
151
 * ```
152
 *
153
 * ```php
154
 * import(__DIR__ . '/config/hosts.yaml');
155
 * ```
156
 *
157
 * @throws Exception
158
 */
159
function import(string $file): void
160
{
161
    Importer::import($file);
162
}
163 15
164
/**
165 15
 * Set task description.
166 15
 */
167
function desc(?string $title = null): ?string
168
{
169 15
    static $store = null;
170 15
171 9
    if ($title === null) {
172 9
        return $store;
173
    } else {
174
        return $store = $title;
175
    }
176
}
177 15
178 15
/**
179
 * Define a new task and save to tasks list.
180 15
 *
181 8
 * Alternatively get a defined task.
182 8
 *
183
 * @param string $name Name of current task.
184
 * @param callable():void|array|null $body Callable task, array of other tasks names or nothing to get a defined tasks
185 15
 */
186
function task(string $name, $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)) {
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);
0 ignored issues
show
Bug introduced by
$body of type callable is incompatible with the type array expected by parameter $group of Deployer\Task\GroupTask::setGroup(). ( Ignorable by Annotation )

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

210
            $existingTask->setGroup(/** @scrutinizer ignore-type */ $body);
Loading history...
211
        } elseif ($existingTask instanceof Task) {
0 ignored issues
show
introduced by
$existingTask is always a sub-type of Deployer\Task\Task.
Loading history...
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():void $do The task to be run.
235
 *
236 8
 * @return Task|null
237 8
 */
238 8
function before(string $task, $do)
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():void $do The task to be run.
255
 *
256
 * @return Task|null
257
 */
258
function after(string $task, $do)
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():void $do The task to be run.
276
 *
277
 * @return Task|null
278
 */
279
function fail(string $task, $do)
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
function cd(string $path): void
312
{
313
    set('working_path', parse($path));
314
}
315
316 8
/**
317 8
 * Change the current user.
318
 *
319 8
 * Usage:
320
 * ```php
321
 * $restore = become('deployer');
322
 *
323
 * // do something
324
 *
325
 * $restore(); // revert back to the previous user
326
 * ```
327
 *
328
 * @param string $user
329
 * @return \Closure
330
 * @throws Exception
331
 */
332
function become(string $user): \Closure
333
{
334
    $currentBecome = get('become');
335 8
    set('become', $user);
336
    return function () use ($currentBecome) {
337
        set('become', $currentBecome);
338
    };
339
}
340
341
/**
342
 * Execute a callback within a specific directory and revert back to the initial working directory.
343
 *
344
 * @return mixed|null Return value of the $callback function or null if callback doesn't return anything
345
 * @throws Exception
346
 */
347
function within(string $path, callable $callback)
348
{
349 6
    $lastWorkingPath = get('working_path', '');
350 6
    try {
351
        set('working_path', parse($path));
352 6
        return $callback();
353 6
    } finally {
354 1
        set('working_path', $lastWorkingPath);
355 1
    }
356
}
357
358 6
/**
359
 * Executes given command on remote host.
360 6
 *
361
 * Examples:
362
 *
363
 * ```php
364
 * run('echo hello world');
365
 * run('cd {{deploy_path}} && git status');
366
 * run('password %secret%', secret: getenv('CI_SECRET'));
367
 * run('curl medv.io', timeout: 5);
368
 * ```
369
 *
370
 * ```php
371
 * $path = run('readlink {{deploy_path}}/current');
372
 * run("echo $path");
373
 * ```
374 8
 *
375
 * @param string $command Command to run on remote host.
376
 * @param array|null $options Array of options will override passed named arguments.
377
 * @param int|null $timeout Sets the process timeout (max. runtime). The timeout in seconds (default: 300 sec; see {{default_timeout}}, `null` to disable).
378
 * @param int|null $idle_timeout Sets the process idle timeout (max. time since last output) in seconds.
379
 * @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.
380
 * @param array|null $env Array of environment variables: `run('echo $KEY', env: ['key' => 'value']);`
381
 * @param bool|null $real_time_output Print command output in real-time.
382
 * @param bool|null $no_throw Don't throw an exception of non-zero exit code.
383
 *
384
 * @throws Exception|RunException|TimeoutException
385
 */
386
function run(string $command, ?array $options = [], ?int $timeout = null, ?int $idle_timeout = null, ?string $secret = null, ?array $env = null, ?bool $real_time_output = false, ?bool $no_throw = false): string
387
{
388
    $namedArguments = [];
389
    foreach (['timeout', 'idle_timeout', 'secret', 'env', 'real_time_output', 'no_throw'] as $arg) {
390
        if ($$arg !== null) {
391
            $namedArguments[$arg] = $$arg;
392
        }
393
    }
394
    $options = array_merge($namedArguments, $options);
395
    $run = function ($command, $options = []): string {
396
        $host = currentHost();
397
398
        $command = parse($command);
399
        $workingPath = get('working_path', '');
400
401
        if (!empty($workingPath)) {
402
            $command = "cd $workingPath && ($command)";
403
        }
404
405
        $env = array_merge_alternate(get('env', []), $options['env'] ?? []);
0 ignored issues
show
Bug introduced by
It seems like get('env', array()) can also be of type null; however, parameter $original of Deployer\Support\array_merge_alternate() does only seem to accept array, 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

405
        $env = array_merge_alternate(/** @scrutinizer ignore-type */ get('env', []), $options['env'] ?? []);
Loading history...
406
        if (!empty($env)) {
407
            $env = env_stringify($env);
408
            $command = "export $env; $command";
409
        }
410
411
        $dotenv = get('dotenv', false);
412
        if (!empty($dotenv)) {
413
            $command = ". $dotenv; $command";
414
        }
415
416
        if ($host instanceof Localhost) {
417
            $process = Deployer::get()->processRunner;
418
            $output = $process->run($host, $command, $options);
419
        } else {
420
            $client = Deployer::get()->sshClient;
421
            $output = $client->run($host, $command, $options);
422
        }
423
424
        return rtrim($output);
425
    };
426
427
    if (preg_match('/^sudo\b/', $command)) {
428
        try {
429
            return $run($command, $options);
430
        } catch (RunException $exception) {
431
            $askpass = get('sudo_askpass', '/tmp/dep_sudo_pass');
432
            $password = get('sudo_pass', false);
433
            if ($password === false) {
434
                writeln("<fg=green;options=bold>run</> $command");
435
                $password = askHiddenResponse(" [sudo] password for {{remote_user}}: ");
436
            }
437
            $run("echo -e '#!/bin/sh\necho \"\$PASSWORD\"' > $askpass");
438
            $run("chmod a+x $askpass");
439
            $command = preg_replace('/^sudo\b/', 'sudo -A', $command);
440
            $output = $run(" SUDO_ASKPASS=$askpass PASSWORD=%sudo_pass% $command", array_merge($options, ['sudo_pass' => escapeshellarg($password)]));
441
            $run("rm $askpass");
442
            return $output;
443
        }
444
    } else {
445
        return $run($command, $options);
446
    }
447
}
448
449
450
/**
451
 * Execute commands on a local machine.
452
 *
453
 * Examples:
454
 *
455
 * ```php
456
 * $user = runLocally('git config user.name');
457
 * runLocally("echo $user");
458
 * ```
459
 *
460
 * @param string $command Command to run on localhost.
461
 * @param array|null $options Array of options will override passed named arguments.
462
 * @param int|null $timeout Sets the process timeout (max. runtime). The timeout in seconds (default: 300 sec, `null` to disable).
463
 * @param int|null $idle_timeout Sets the process idle timeout (max. time since last output) in seconds.
464
 * @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.
465
 * @param array|null $env Array of environment variables: `runLocally('echo $KEY', env: ['key' => 'value']);`
466
 * @param string|null $shell Shell to run in. Default is `bash -s`.
467
 *
468
 * @throws RunException
469
 */
470
function runLocally(string $command, ?array $options = [], ?int $timeout = null, ?int $idle_timeout = null, ?string $secret = null, ?array $env = null, ?string $shell = null): string
471
{
472
    $namedArguments = [];
473
    foreach (['timeout', 'idle_timeout', 'secret', 'env', 'shell'] as $arg) {
474
        if ($$arg !== null) {
475
            $namedArguments[$arg] = $$arg;
476
        }
477
    }
478
    $options = array_merge($namedArguments, $options);
479 4
480 4
    $process = Deployer::get()->processRunner;
481
    $command = parse($command);
482
483
    $env = array_merge_alternate(get('env', []), $options['env'] ?? []);
0 ignored issues
show
Bug introduced by
It seems like get('env', array()) can also be of type null; however, parameter $original of Deployer\Support\array_merge_alternate() does only seem to accept array, 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

483
    $env = array_merge_alternate(/** @scrutinizer ignore-type */ get('env', []), $options['env'] ?? []);
Loading history...
484
    if (!empty($env)) {
485
        $env = env_stringify($env);
486
        $command = "export $env; $command";
487
    }
488
489
    $output = $process->run(new Localhost(), $command, $options);
490
491
    return rtrim($output);
492
}
493
494
/**
495
 * Run test command.
496
 * Example:
497
 *
498 7
 * ```php
499 7
 * if (test('[ -d {{release_path}} ]')) {
500 7
 * ...
501
 * }
502
 * ```
503
 *
504
 */
505
function test(string $command): bool
506
{
507
    $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

507
    $true = '+' . /** @scrutinizer ignore-type */ array_rand(array_flip(['accurate', 'appropriate', 'correct', 'legitimate', 'precise', 'right', 'true', 'yes', 'indeed']));
Loading history...
508
    return trim(run("if $command; then echo $true; fi")) === $true;
509
}
510
511
/**
512
 * Run test command locally.
513
 * Example:
514
 *
515
 *     testLocally('[ -d {{local_release_path}} ]')
516
 *
517
 */
518
function testLocally(string $command): bool
519
{
520 10
    return runLocally("if $command; then echo +true; fi") === '+true';
521
}
522
523
/**
524
 * Iterate other hosts, allowing to call run a func in callback.
525
 *
526
 * ```php
527
 * on(select('stage=prod, role=db'), function ($host) {
528
 *     ...
529
 * });
530
 * ```
531 12
 *
532 12
 * ```php
533
 * on(host('example.org'), function ($host) {
534 6
 *     ...
535
 * });
536 12
 * ```
537
 *
538
 * ```php
539
 * on(Deployer::get()->hosts, function ($host) {
540
 *     ...
541
 * });
542
 * ```
543
 *
544
 * @param Host|Host[] $hosts
545
 */
546
function on($hosts, callable $callback): void
547
{
548
    if (!is_array($hosts) && !($hosts instanceof \Traversable)) {
549
        $hosts = [$hosts];
550
    }
551
552
    foreach ($hosts as $host) {
553
        if ($host instanceof Host) {
554
            $host->config()->load();
555
            Context::push(new Context($host));
556
            try {
557
                $callback($host);
558
                $host->config()->save();
559
            } catch (GracefulShutdownException $e) {
560
                Deployer::get()->messenger->renderException($e, $host);
561
            } finally {
562 10
                Context::pop();
563
            }
564
        } else {
565 10
            throw new \InvalidArgumentException("Function on can iterate only on Host instances.");
566
        }
567
    }
568
}
569
570
/**
571
 * Runs a task.
572
 * ```php
573
 * invoke('deploy:symlink');
574
 * ```
575
 *
576
 * @throws Exception
577 4
 */
578
function invoke(string $taskName): void
579
{
580 4
    $task = Deployer::get()->tasks->get($taskName);
581
    Deployer::get()->messenger->startTask($task);
582
    $task->run(Context::get());
0 ignored issues
show
Bug introduced by
It seems like Deployer\Task\Context::get() can also be of type false; however, parameter $context of Deployer\Task\Task::run() does only seem to accept Deployer\Task\Context, 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

582
    $task->run(/** @scrutinizer ignore-type */ Context::get());
Loading history...
583
    Deployer::get()->messenger->endTask($task);
584
}
585
586
/**
587
 * Upload files or directories to host.
588
 *
589
 * > To upload the _contents_ of a directory, include a trailing slash (eg `upload('build/', '{{release_path}}/public');`).
590
 * > Without the trailing slash, the build directory itself will be uploaded (resulting in `{{release_path}}/public/build`).
591
 *
592 1
 *  The `$config` array supports the following keys:
593
 *
594 1
 * - `flags` for overriding the default `-azP` passed to the `rsync` command
595
 * - `options` with additional flags passed directly to the `rsync` command
596
 * - `timeout` for `Process::fromShellCommandline()` (`null` by default)
597
 * - `progress_bar` to display upload/download progress
598 1
 * - `display_stats` to display rsync set of statistics
599
 *
600
 * Note: due to the way php escapes command line arguments, list-notation for the rsync `--exclude={'file','anotherfile'}` option will not work.
601
 * A workaround is to add a separate `--exclude=file` argument for each exclude to `options` (also, _do not_ wrap the filename/filter in quotes).
602
 * An alternative might be to write the excludes to a temporary file (one per line) and use `--exclude-from=temporary_file` argument instead.
603 1
 *
604
 * @param string|string[] $source
605 1
 * @param array $config
606 1
 * @phpstan-param array{flags?: string, options?: array, timeout?: int|null, progress_bar?: bool, display_stats?: bool} $config
607
 *
608 1
 * @throws RunException
609 1
 */
610
function upload($source, string $destination, array $config = []): void
611
{
612
    $rsync = Deployer::get()->rsync;
613 1
    $host = currentHost();
614
    $source = is_array($source) ? array_map('Deployer\parse', $source) : parse($source);
615
    $destination = parse($destination);
616
617
    if ($host instanceof Localhost) {
618
        $rsync->call($host, $source, $destination, $config);
619
    } else {
620
        $rsync->call($host, $source, "{$host->connectionString()}:$destination", $config);
621
    }
622
}
623
624
/**
625
 * Download file or directory from host
626
 *
627
 * @param array $config
628
 *
629
 * @throws RunException
630
 */
631
function download(string $source, string $destination, array $config = []): void
632
{
633
    $rsync = Deployer::get()->rsync;
634
    $host = currentHost();
635
    $source = parse($source);
636
    $destination = parse($destination);
637
638
    if ($host instanceof Localhost) {
639
        $rsync->call($host, $source, $destination, $config);
640
    } else {
641
        $rsync->call($host, "{$host->connectionString()}:$source", $destination, $config);
642
    }
643
}
644
645
/**
646
 * Writes an info message.
647
 */
648
function info(string $message): void
649
{
650
    writeln("<fg=green;options=bold>info</> " . parse($message));
651
}
652
653
/**
654
 * Writes an warning message.
655
 */
656
function warning(string $message): void
657
{
658
    $message = "<fg=yellow;options=bold>warning</> <comment>$message</comment>";
659
660
    if (Context::has()) {
661
        writeln($message);
662
    } else {
663
        Deployer::get()->output->writeln($message);
664
    }
665
}
666
667
/**
668
 * Writes a message to the output and adds a newline at the end.
669
 */
670
function writeln(string $message, int $options = 0): void
671
{
672
    $host = currentHost();
673
    output()->writeln("[$host] " . parse($message), $options);
674
}
675
676
/**
677
 * Parse set values.
678
 */
679
function parse(string $value): string
680
{
681
    return Context::get()->getConfig()->parse($value);
682
}
683
684
/**
685
 * Setup configuration option.
686
 * @param mixed $value
687
 * @throws Exception
688
 */
689
function set(string $name, $value): void
690
{
691
    if (!Context::has()) {
692
        Deployer::get()->config->set($name, $value);
693
    } else {
694
        Context::get()->getConfig()->set($name, $value);
695
    }
696
}
697
698
/**
699
 * Merge new config params to existing config array.
700
 *
701
 * @param array $array
702
 */
703
function add(string $name, array $array): void
704
{
705
    if (!Context::has()) {
706
        Deployer::get()->config->add($name, $array);
707
    } else {
708
        Context::get()->getConfig()->add($name, $array);
709
    }
710
}
711
712
/**
713
 * Get configuration value.
714
 *
715
 * @param mixed|null $default
716
 *
717
 * @return mixed
718 5
 */
719
function get(string $name, $default = null)
720
{
721
    if (!Context::has()) {
722
        return Deployer::get()->config->get($name, $default);
723
    } else {
724
        return Context::get()->getConfig()->get($name, $default);
725
    }
726
}
727 8
728
/**
729
 * Check if there is such configuration option.
730
 */
731
function has(string $name): bool
732
{
733
    if (!Context::has()) {
734
        return Deployer::get()->config->has($name);
735
    } else {
736
        return Context::get()->getConfig()->has($name);
737
    }
738 3
}
739
740
function ask(string $message, ?string $default = null, ?array $autocomplete = null): ?string
741
{
742
    if (defined('DEPLOYER_NO_ASK')) {
743 5
        throw new WillAskUser($message);
744 5
    }
745
    Context::required(__FUNCTION__);
746
747 5
    if (output()->isQuiet()) {
748
        return $default;
749
    }
750
751
    if (Deployer::isWorker()) {
752 4
        return Deployer::proxyCallToMaster(currentHost(), __FUNCTION__, ...func_get_args());
753
    }
754
755
    /** @var QuestionHelper */
756
    $helper = Deployer::get()->getHelper('question');
757 4
758 4
    $tag = currentHost()->getTag();
759
    $message = parse($message);
760
    $message = "[$tag] <question>$message</question> " . (($default === null) ? "" : "(default: $default) ");
761
762
    $question = new Question($message, $default);
763 4
    if (!empty($autocomplete)) {
764
        $question->setAutocompleterValues($autocomplete);
765
    }
766
767
    return $helper->ask(input(), output(), $question);
0 ignored issues
show
Bug introduced by
The method ask() does not exist on Symfony\Component\Console\Helper\HelperInterface. It seems like you code against a sub-type of Symfony\Component\Console\Helper\HelperInterface such as Symfony\Component\Console\Helper\QuestionHelper. ( Ignorable by Annotation )

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

767
    return $helper->/** @scrutinizer ignore-call */ ask(input(), output(), $question);
Loading history...
768
}
769
770
/**
771
 * @param mixed $default
772
 * @return mixed
773
 * @throws Exception
774
 */
775
function askChoice(string $message, array $availableChoices, $default = null, bool $multiselect = false)
776
{
777
    if (defined('DEPLOYER_NO_ASK')) {
778
        throw new WillAskUser($message);
779
    }
780
    Context::required(__FUNCTION__);
781
782
    if (empty($availableChoices)) {
783
        throw new \InvalidArgumentException('Available choices should not be empty');
784
    }
785
786
    if ($default !== null && !array_key_exists($default, $availableChoices)) {
787
        throw new \InvalidArgumentException('Default choice is not available');
788
    }
789
790
    if (output()->isQuiet()) {
791
        if ($default === null) {
792
            $default = key($availableChoices);
793
        }
794
        return [$default => $availableChoices[$default]];
795
    }
796
797
    if (Deployer::isWorker()) {
798
        return Deployer::proxyCallToMaster(currentHost(), __FUNCTION__, ...func_get_args());
799
    }
800
801
    /** @var QuestionHelper */
802
    $helper = Deployer::get()->getHelper('question');
803
804
    $tag = currentHost()->getTag();
805
    $message = parse($message);
806
    $message = "[$tag] <question>$message</question> " . (($default === null) ? "" : "(default: $default) ");
807
808
    $question = new ChoiceQuestion($message, $availableChoices, $default);
809
    $question->setMultiselect($multiselect);
810
811
    return $helper->ask(input(), output(), $question);
812
}
813
814
function askConfirmation(string $message, bool $default = false): bool
815
{
816
    if (defined('DEPLOYER_NO_ASK')) {
817
        throw new WillAskUser($message);
818
    }
819
    Context::required(__FUNCTION__);
820
821
    if (output()->isQuiet()) {
822
        return $default;
823
    }
824
825
    if (Deployer::isWorker()) {
826
        return Deployer::proxyCallToMaster(currentHost(), __FUNCTION__, ...func_get_args());
827
    }
828
829
    /** @var QuestionHelper */
830
    $helper = Deployer::get()->getHelper('question');
831
832
    $yesOrNo = $default ? 'Y/n' : 'y/N';
833
    $tag = currentHost()->getTag();
834
    $message = parse($message);
835
    $message = "[$tag] <question>$message</question> [$yesOrNo] ";
836
837
    $question = new ConfirmationQuestion($message, $default);
838
839
    return $helper->ask(input(), output(), $question);
840
}
841
842
function askHiddenResponse(string $message): string
843
{
844
    if (defined('DEPLOYER_NO_ASK')) {
845
        throw new WillAskUser($message);
846
    }
847
    Context::required(__FUNCTION__);
848
849
    if (output()->isQuiet()) {
850
        return '';
851
    }
852
853
    if (Deployer::isWorker()) {
854
        return (string) Deployer::proxyCallToMaster(currentHost(), __FUNCTION__, ...func_get_args());
855
    }
856
857
    /** @var QuestionHelper */
858
    $helper = Deployer::get()->getHelper('question');
859
860
    $tag = currentHost()->getTag();
861
    $message = parse($message);
862
    $message = "[$tag] <question>$message</question> ";
863
864
    $question = new Question($message);
865
    $question->setHidden(true);
866
    $question->setHiddenFallback(false);
867
868
    return (string) $helper->ask(input(), output(), $question);
869
}
870
871
function input(): InputInterface
872
{
873
    return Deployer::get()->input;
874
}
875
876
function output(): OutputInterface
877
{
878
    return Deployer::get()->output;
879
}
880
881
/**
882
 * Check if command exists
883
 *
884
 * @throws RunException
885
 */
886
function commandExist(string $command): bool
887
{
888
    return test("hash $command 2>/dev/null");
889
}
890
891
/**
892
 * @throws RunException
893
 */
894
function commandSupportsOption(string $command, string $option): bool
895
{
896
    $man = run("(man $command 2>&1 || $command -h 2>&1 || $command --help 2>&1) | grep -- $option || true");
897
    if (empty($man)) {
898
        return false;
899
    }
900
    return str_contains($man, $option);
901
}
902
903
/**
904
 * @throws RunException
905
 */
906
function which(string $name): string
907
{
908
    $nameEscaped = escapeshellarg($name);
909
910
    // Try `command`, should cover all Bourne-like shells
911
    // Try `which`, should cover most other cases
912
    // Fallback to `type` command, if the rest fails
913
    $path = run("command -v $nameEscaped || which $nameEscaped || type -p $nameEscaped");
914
    if (empty($path)) {
915
        throw new \RuntimeException("Can't locate [$nameEscaped] - neither of [command|which|type] commands are available");
916
    }
917
918
    // Deal with issue when `type -p` outputs something like `type -ap` in some implementations
919
    return trim(str_replace("$name is", "", $path));
920
921
}
922
923
/**
924
 * Returns remote environments variables as an array.
925
 * ```php
926
 * $remotePath = remoteEnv()['PATH'];
927
 * run('echo $PATH', env: ['PATH' => "/home/user/bin:$remotePath"]);
928
 * ```
929
 */
930
function remoteEnv(): array
931
{
932
    $vars = [];
933
    $data = run('env');
934
    foreach (explode("\n", $data) as $line) {
935
        [$name, $value] = explode('=', $line, 2);
936
        $vars[$name] = $value;
937
    }
938
    return $vars;
939
}
940
941
/**
942
 * Creates a new exception.
943
 */
944
function error(string $message): Exception
945
{
946
    return new Exception(parse($message));
947
}
948
949
/**
950
 * Returns current timestamp in UTC timezone in ISO8601 format.
951
 */
952
function timestamp(): string
953
{
954
    return (new \DateTime('now', new \DateTimeZone('UTC')))->format(\DateTime::ISO8601);
955
}
956
957
/**
958
 * Example usage:
959
 * ```php
960
 * $result = fetch('{{domain}}', info: $info);
961
 * var_dump($info['http_code'], $result);
962
 * ```
963
 */
964
function fetch(string $url, string $method = 'get', array $headers = [], ?string $body = null, ?array &$info = null, bool $nothrow = false): string
965
{
966
    $url = parse($url);
967
    if (strtolower($method) === 'get') {
968
        $http = Httpie::get($url);
969
    } elseif (strtolower($method) === 'post') {
970
        $http = Httpie::post($url);
971
    } else {
972
        throw new \InvalidArgumentException("Unknown method \"$method\".");
973
    }
974
    $http = $http->nothrow($nothrow);
975
    foreach ($headers as $key => $value) {
976
        $http = $http->header($key, $value);
977
    }
978
    if ($body !== null) {
979
        $http = $http->body($body);
980
    }
981
    return $http->send($info);
982
}
983