• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

eiriksm / cosy-composer / 14650775326

24 Apr 2025 08:12PM UTC coverage: 86.922% (-0.2%) from 87.114%
14650775326

push

github

web-flow
Make sure the config branch has consistent behaviour with default branch (#423)

9 of 14 new or added lines in 1 file covered. (64.29%)

1 existing line in 1 file now uncovered.

1974 of 2271 relevant lines covered (86.92%)

44.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.62
/src/CosyComposer.php
1
<?php
2

3
namespace eiriksm\CosyComposer;
4

5
use eiriksm\CosyComposer\Exceptions\ChdirException;
6
use eiriksm\CosyComposer\Exceptions\GitCloneException;
7
use eiriksm\CosyComposer\Exceptions\OutsideProcessingHoursException;
8
use eiriksm\CosyComposer\ListFilterer\DevDepsOnlyFilterer;
9
use eiriksm\CosyComposer\ListFilterer\IndirectWithDirectFilterer;
10
use eiriksm\CosyComposer\Providers\Bitbucket;
11
use eiriksm\CosyComposer\Providers\PublicGithubWrapper;
12
use eiriksm\CosyComposer\Updater\IndividualUpdater;
13
use GuzzleHttp\Psr7\Request;
14
use Http\Adapter\Guzzle7\Client as GuzzleClient;
15
use Http\Client\HttpClient;
16
use League\Flysystem\FilesystemAdapter;
17
use Symfony\Component\Process\Process;
18
use Violinist\AllowListHandler\AllowListHandler;
19
use Violinist\ComposerLockData\ComposerLockData;
20
use Violinist\ComposerUpdater\Exception\NotUpdatedException;
21
use Violinist\Config\Config;
22
use eiriksm\ViolinistMessages\ViolinistMessages;
23
use Github\Client;
24
use Github\Exception\RuntimeException;
25
use Github\Exception\ValidationFailedException;
26
use League\Flysystem\Local\LocalFilesystemAdapter;
27
use Psr\Log\LoggerInterface;
28
use Violinist\RepoAndTokenToCloneUrl\ToCloneUrl;
29
use Violinist\Slug\Slug;
30
use Violinist\TimeFrameHandler\Handler;
31
use Wa72\SimpleLogger\ArrayLogger;
32

33
class CosyComposer
34
{
35
    use ComposerInstallTrait;
36
    use PrCounterTrait;
37
    use GitCommandsTrait;
38
    use SlugAwareTrait;
39
    use TokenAwareTrait;
40
    use AssigneesAllowedTrait;
41
    use TemporaryDirectoryAwareTrait;
42

43
    const UPDATE_ALL = 'update_all';
44

45
    const UPDATE_INDIVIDUAL = 'update_individual';
46

47
    private $urlArray;
48

49
    /**
50
     * @var bool|string
51
     */
52
    private $lockFileContents;
53

54
    /**
55
     * @var ProviderFactory
56
     */
57
    protected $providerFactory;
58

59
    /**
60
     * @var \eiriksm\CosyComposer\CommandExecuter
61
     */
62
    protected $executer;
63

64
    /**
65
     * @var ComposerFileGetter
66
     */
67
    protected $composerGetter;
68

69
    /**
70
     * @var string
71
     */
72
    protected $cwd;
73

74
    /**
75
     * @var string
76
     */
77
    private $forkUser;
78

79
    /**
80
     * @var ViolinistMessages
81
     */
82
    private $messageFactory;
83

84
    /**
85
     * @var string
86
     */
87
    protected $composerJsonDir;
88

89
    /**
90
     * @var LoggerInterface
91
     */
92
    protected $logger;
93

94
    /**
95
     * @var null|\Violinist\ProjectData\ProjectData
96
     */
97
    protected $project;
98

99
    /**
100
     * @var HttpClient
101
     */
102
    protected $httpClient;
103

104
    /**
105
     * @var string
106
     */
107
    protected $tokenUrl;
108

109
    /**
110
     * @var bool
111
     */
112
    private $isPrivate = false;
113

114
    /**
115
     * @var SecurityCheckerFactory
116
     */
117
    private $checkerFactory;
118

119
    /**
120
     * @var ProviderInterface
121
     */
122
    private $client;
123

124
    /**
125
     * @var ProviderInterface
126
     */
127
    private $privateClient;
128

129
    /**
130
     * @var string
131
     */
132
    private $hostName;
133

134
    /**
135
     * @var PrParamsCreator
136
     */
137
    private $prParamsCreator;
138

139
    /**
140
     * @param array $tokens
141
     */
142
    public function setTokens(array $tokens)
143
    {
144
        $this->tokens = $tokens;
1✔
145
    }
146

147
    /**
148
     * @return SecurityCheckerFactory
149
     */
150
    public function getCheckerFactory()
151
    {
152
        return $this->checkerFactory;
185✔
153
    }
154

155
    /**
156
     * @param string $tokenUrl
157
     */
158
    public function setTokenUrl($tokenUrl)
159
    {
160
        $this->tokenUrl = $tokenUrl;
185✔
161
    }
162

163
    /**
164
     * @param \Violinist\ProjectData\ProjectData|null $project
165
     */
166
    public function setProject($project)
167
    {
168
        $this->project = $project;
185✔
169
    }
170

171
    /**
172
     * @return LoggerInterface
173
     */
174
    public function getLogger()
175
    {
176
        if (!$this->logger instanceof LoggerInterface) {
173✔
177
            $this->logger = new ArrayLogger();
172✔
178
        }
179
        return $this->logger;
173✔
180
    }
181

182
    /**
183
     * @param LoggerInterface $logger
184
     */
185
    public function setLogger(LoggerInterface $logger)
186
    {
187
        $this->logger = $logger;
1✔
188
    }
189

190
    /**
191
     * @return HttpClient
192
     */
193
    public function getHttpClient()
194
    {
195
        if (!$this->httpClient instanceof HttpClient) {
34✔
196
            $this->httpClient = new GuzzleClient();
×
197
        }
198
        return $this->httpClient;
34✔
199
    }
200

201
    /**
202
     * @param HttpClient $httpClient
203
     */
204
    public function setHttpClient(HttpClient $httpClient)
205
    {
206
        $this->httpClient = $httpClient;
185✔
207
    }
208

209
    /**
210
     * @return string
211
     */
212
    public function getCwd()
213
    {
214
        return $this->cwd;
172✔
215
    }
216

217
    /**
218
     * @param \eiriksm\CosyComposer\CommandExecuter $executer
219
     */
220
    public function setExecuter($executer)
221
    {
222
        $this->executer = $executer;
177✔
223
    }
224

225
    /**
226
     * @param ProviderFactory $providerFactory
227
     */
228
    public function setProviderFactory(ProviderFactory $providerFactory)
229
    {
230
        $this->providerFactory = $providerFactory;
185✔
231
    }
232

233

234
    /**
235
     * CosyComposer constructor.
236
     */
237
    public function __construct(CommandExecuter $executer)
238
    {
239
        $tmpdir_name = uniqid();
186✔
240
        $this->setTmpDir(sprintf('/tmp/%s', $tmpdir_name));
186✔
241
        $this->messageFactory = new ViolinistMessages();
186✔
242
        $this->executer = $executer;
186✔
243
        $this->checkerFactory = new SecurityCheckerFactory();
186✔
244
    }
245

246
    public function setUrl($url = null)
247
    {
248
        if (!empty($url)) {
185✔
249
            $url = preg_replace('/\.git$/', '', $url);
185✔
250
        }
251
        $slug_url_obj = parse_url($url);
185✔
252
        if (empty($slug_url_obj['port'])) {
185✔
253
            // Set it based on scheme.
254
            switch ($slug_url_obj['scheme']) {
185✔
255
                case 'http':
185✔
256
                    $slug_url_obj['port'] = 80;
1✔
257
                    break;
1✔
258

259
                case 'https':
185✔
260
                    $slug_url_obj['port'] = 443;
185✔
261
                    break;
185✔
262
            }
263
        }
264
        $this->urlArray = $slug_url_obj;
185✔
265
        $providers = Slug::getSupportedProviders();
185✔
266
        if (!empty($slug_url_obj['host'])) {
185✔
267
            $providers = array_merge($providers, [$slug_url_obj['host']]);
185✔
268
        }
269
        $this->setSlug(Slug::createFromUrlAndSupportedProviders($url, $providers));
185✔
270
    }
271

272
    /**
273
     * @deprecated Use ::setAuthentication instead.
274
     *
275
     * @see CosyComposer::setAuthentication
276
     */
277
    public function setGithubAuth($user, $pass)
278
    {
279
        $this->setAuthentication($user);
1✔
280
    }
281

282
    /**
283
     * @deprecated use ::setAuthentication instead.
284
     */
285
    public function setUserToken($user_token)
286
    {
287
        $this->setAuthentication($user_token);
1✔
288
    }
289

290
  /**
291
   * Set a user to fork to.
292
   *
293
   * @param string $user
294
   */
295
    public function setForkUser($user)
296
    {
297
        $this->forkUser = $user;
1✔
298
    }
299

300
    protected function handleTimeIntervalSetting(Config $config)
301
    {
302
        if (Handler::isAllowed($config)) {
168✔
303
            return;
166✔
304
        }
305
        throw new OutsideProcessingHoursException('Current hour is inside timeframe disallowed');
1✔
306
    }
307

308
    public function handleDrupalContribSa($cdata)
309
    {
310
        if (!getenv('DRUPAL_CONTRIB_SA_PATH')) {
168✔
311
            return;
168✔
312
        }
313
        $symfony_dir = sprintf('%s/.symfony/cache/security-advisories/drupal', getenv('HOME'));
×
314
        if (!file_exists($symfony_dir)) {
×
315
            $mkdir = $this->execCommand(['mkdir', '-p', $symfony_dir]);
×
316
            if ($mkdir) {
×
317
                return;
×
318
            }
319
        }
320
        $contrib_sa_dir = getenv('DRUPAL_CONTRIB_SA_PATH');
×
321
        if (empty($cdata->repositories)) {
×
322
            return;
×
323
        }
324
        foreach ($cdata->repositories as $repository) {
×
325
            if (empty($repository->url)) {
×
326
                continue;
×
327
            }
328
            if ($repository->url === 'https://packages.drupal.org/8') {
×
329
                $process = Process::fromShellCommandline('rsync -aq ' . sprintf('%s/sa_yaml/8/drupal/*', $contrib_sa_dir) .  " $symfony_dir/");
×
330
                $process->run();
×
331
            }
332
            if ($repository->url === 'https://packages.drupal.org/7') {
×
333
                $process = Process::fromShellCommandline('rsync -aq ' . sprintf('%s/sa_yaml/7/drupal/*', $contrib_sa_dir) .  " $symfony_dir/");
×
334
                $process->run();
×
335
            }
336
        }
337
    }
338

339
    /**
340
     * Export things.
341
     */
342
    protected function exportEnvVars()
343
    {
344
        if (!$this->project) {
172✔
345
            return;
×
346
        }
347
        $env = $this->project->getEnvString();
172✔
348
        if (empty($env)) {
172✔
349
            return;
166✔
350
        }
351
        // One per line.
352
        $env_array = preg_split("/\r\n|\n|\r/", $env);
6✔
353
        if (empty($env_array)) {
6✔
354
            return;
×
355
        }
356
        foreach ($env_array as $env_string) {
6✔
357
            if (empty($env_string)) {
6✔
358
                continue;
2✔
359
            }
360
            $env_parts = explode('=', $env_string, 2);
6✔
361
            if (count($env_parts) != 2) {
6✔
362
                continue;
×
363
            }
364
            // We do not allow to override ENV vars.
365
            $key = $env_parts[0];
6✔
366
            $existing_env = getenv($key);
6✔
367
            if ($existing_env) {
6✔
368
                $this->getLogger()->log('info', new Message("The ENV variable $key was skipped because it exists and can not be overwritten"));
3✔
369
                continue;
3✔
370
            }
371
            $value = $env_parts[1];
6✔
372
            $this->getLogger()->log('info', new Message("Exporting ENV variable $key: $value"));
6✔
373
            putenv($env_string);
6✔
374
            $_ENV[$key] = $value;
6✔
375
        }
376
    }
377

378
    protected function closeOutdatedPrsForPackage($package_name, $current_version, Config $config, $pr_id, $prs_named, $default_branch)
379
    {
380
        $fake_item = (object) [
6✔
381
            'name' => $package_name,
6✔
382
            'version' => $current_version,
6✔
383
            'latest' => '',
6✔
384
        ];
6✔
385
        $branch_name_prefix = Helpers::createBranchName($fake_item, false, $config);
6✔
386
        foreach ($prs_named as $branch_name => $pr) {
6✔
387
            if (!empty($pr["base"]["ref"])) {
6✔
388
                // The base ref should be what we are actually using for merge requests.
389
                if ($pr["base"]["ref"] != $default_branch) {
×
390
                    continue;
×
391
                }
392
            }
393
            if ($pr["number"] == $pr_id) {
6✔
394
                // We really don't want to close the one we are considering as the latest one, do we?
395
                continue;
6✔
396
            }
397
            // We are just going to assume, if the number of the PR does not match. And the branch name does
398
            // indeed "match", well. Match as in it updates the exact package from the exact same version. Then
399
            // the current/recent PR will update to a newer version. Or it could also be that the branch was
400
            // created while the project was using one PR per version, and then they switched. Either way. These
401
            // two scenarios are both scenarios we want to handle in such a way that we are closing this PR that
402
            // is matching.
403
            if (strpos($branch_name, $branch_name_prefix) === false) {
3✔
404
                continue;
×
405
            }
406
            $comment = $this->messageFactory->getPullRequestClosedMessage($pr_id);
3✔
407
            $pr_number = $pr['number'];
3✔
408
            $this->getLogger()->log('info', new Message("Trying to close PR number $pr_number since it has been superseded by $pr_id"));
3✔
409
            try {
410
                $this->getPrClient()->closePullRequestWithComment($this->slug, $pr_number, $comment);
3✔
411
                $this->getLogger()->log('info', new Message("Successfully closed PR $pr_number"));
3✔
412
            } catch (\Throwable $e) {
×
413
                $msg = $e->getMessage();
×
414
                $this->getLogger()->log('error', new Message("Caught an exception trying to close pr $pr_number. The message was '$msg'"));
×
415
            }
416
        }
417
    }
418

419
    public function setViolinistHostname(string $hostname)
420
    {
421
        $this->hostName = $hostname;
185✔
422
    }
423

424
    public function getLocalAdapterForTempDir(string $directory) : FilesystemAdapter
425
    {
426
        $this->setTmpDir($directory);
171✔
427
        $composer_json_dir = $this->tmpDir;
171✔
428
        if ($this->project && $this->project->getComposerJsonDir()) {
171✔
NEW
429
            $composer_json_dir = sprintf('%s/%s', $this->tmpDir, $this->project->getComposerJsonDir());
×
430
        }
431
        $this->composerJsonDir = $composer_json_dir;
171✔
432
        return new LocalFilesystemAdapter($this->composerJsonDir);
171✔
433
    }
434

435
    /**
436
     * @throws \eiriksm\CosyComposer\Exceptions\ChdirException
437
     * @throws \eiriksm\CosyComposer\Exceptions\GitCloneException
438
     * @throws \InvalidArgumentException
439
     * @throws \Exception
440
     * @throws \Throwable
441
     */
442
    public function run()
443
    {
444
        // Always start by making sure the .ssh directory exists.
445
        $directory = sprintf('%s/.ssh', getenv('HOME'));
172✔
446
        if (!file_exists($directory)) {
172✔
447
            if (!@mkdir($directory, 0700) && !is_dir($directory)) {
1✔
448
                throw new \RuntimeException(sprintf('Directory "%s" was not created', $directory));
×
449
            }
450
        }
451
        // Export the environment variables if needed.
452
        $this->exportEnvVars();
172✔
453
        if ($this->hostName) {
172✔
454
            $this->log(sprintf('Running update check on %s', $this->hostName));
172✔
455
        }
456
        if (!empty($_SERVER['violinist_revision'])) {
172✔
457
            $this->log(sprintf('Queue starter revision %s', $_SERVER['violinist_revision']));
×
458
        }
459
        if (!empty($_SERVER['queue_runner_revision'])) {
172✔
460
            $this->log(sprintf('Queue runner revision %s', $_SERVER['queue_runner_revision']));
×
461
        }
462
        // Support an alternate composer version based on env var.
463
        if (!empty($_ENV['ALTERNATE_COMPOSER_PATH'])) {
172✔
464
            $allow_list = [
×
465
                '/usr/local/bin/composer22',
×
466
            ];
×
467
            if (!in_array($_ENV['ALTERNATE_COMPOSER_PATH'], $allow_list)) {
×
468
                throw new \InvalidArgumentException('The alternate composer path is not allowed');
×
469
            }
470
            $this->log('Trying to use composer from ' . $_ENV['ALTERNATE_COMPOSER_PATH']);
×
471
            if (file_exists('/usr/local/bin/composer')) {
×
472
                rename('/usr/local/bin/composer', '/usr/local/bin/composer.bak');
×
473
            }
474
            copy($_ENV['ALTERNATE_COMPOSER_PATH'], '/usr/local/bin/composer');
×
475
            chmod('/usr/local/bin/composer', 0755);
×
476
        }
477
        // Try to get the php version as well.
478
        $this->execCommand(['php', '--version']);
172✔
479
        $this->log($this->getLastStdOut());
172✔
480
        // Try to get the composer version as well.
481
        $this->execCommand(['composer', '--version']);
172✔
482
        $this->log($this->getLastStdOut());
172✔
483
        $this->log(sprintf('Starting update check for %s', $this->slug->getSlug()));
172✔
484
        $user_name = $this->slug->getUserName();
172✔
485
        $user_repo = $this->slug->getUserRepo();
172✔
486
        $hostname = $this->slug->getProvider();
172✔
487
        $url = null;
172✔
488
        // Make sure we accept the fingerprint of whatever we are cloning.
489
        $this->execCommand(['ssh-keyscan', '-t', 'rsa', $hostname, '>>', '~/.ssh/known_hosts']);
172✔
490
        if (!empty($_SERVER['private_key'])) {
172✔
491
            $this->log('Checking for existing private key');
×
492
            $filename = "$directory/id_rsa";
×
493
            if (!file_exists($filename)) {
×
494
                $this->log('Installing private key');
×
495
                file_put_contents($filename, $_SERVER['private_key']);
×
496
                $this->execCommand(['chmod', '600', $filename], false);
×
497
            }
498
        }
499
        $is_bitbucket = false;
172✔
500
        $bitbucket_user = null;
172✔
501
        $url = ToCloneUrl::fromRepoAndToken($this->slug->getUrl(), $this->userToken);
172✔
502
        switch ($hostname) {
503
            case 'bitbucket.org':
172✔
504
                $is_bitbucket = true;
2✔
505
                if (Bitbucket::tokenIndicatesUserAppPassword($this->userToken)) {
2✔
506
                    // The username will now be the thing before the colon.
507
                    [$bitbucket_user, $this->userToken] = explode(':', $this->userToken);
1✔
508
                }
509
                break;
2✔
510

511
            default:
512
                // Use the upstream package for this.
513
                break;
170✔
514
        }
515
        $urls = [
172✔
516
            $url,
172✔
517
        ];
172✔
518
        // We also want to check what happens if we append .git to the URL. This can be a problem in newer
519
        // versions of git, that git does not accept redirects.
520
        $length = strlen('.git');
172✔
521
        $ends_with_git = substr($url, -$length) === '.git';
172✔
522
        if (!$ends_with_git) {
172✔
523
            $urls[] = "$url.git";
170✔
524
        }
525
        $this->log('Cloning repository');
172✔
526
        foreach ($urls as $url) {
172✔
527
            $clone_result = $this->execCommand(['git', 'clone', '--depth=1', $url, $this->tmpDir], false, 240);
172✔
528
            if (!$clone_result) {
172✔
529
                break;
171✔
530
            }
531
        }
532
        if ($clone_result) {
172✔
533
            // We had a problem.
534
            $this->log($this->getLastStdOut());
1✔
535
            $this->log($this->getLastStdErr());
1✔
536
            throw new GitCloneException('Problem with the execCommand git clone. Exit code was ' . $clone_result);
1✔
537
        }
538
        $this->log('Repository cloned');
171✔
539
        $local_adapter = $this->getLocalAdapterForTempDir($this->tmpDir);
171✔
540
        $this->chdir($this->composerJsonDir);
171✔
541
        if (!empty($_ENV['config_branch'])) {
171✔
542
            $config_branch = $_ENV['config_branch'];
8✔
543
            $this->log('Changing to config branch: ' . $config_branch);
8✔
544
            $tmpdir = sprintf('/tmp/%s', uniqid('', true));
8✔
545
            $clone_result = $this->execCommand(['git', 'clone', '--depth=1', $url, $tmpdir, '-b', $config_branch], false, 120);
8✔
546
            if (!$clone_result) {
8✔
547
                $local_adapter = $this->getLocalAdapterForTempDir($tmpdir);
8✔
548
            } else {
NEW
549
                $this->log($this->getLastStdOut());
×
NEW
550
                $this->log($this->getLastStdErr());
×
NEW
551
                throw new GitCloneException('Problem with git clone of the config branch. Exit code was ' . $clone_result);
×
552
            }
553
            if (!$this->chdir($this->composerJsonDir)) {
8✔
NEW
554
                throw new ChdirException('Problem with changing dir to the clone dir of the config branch.');
×
555
            }
556
        }
557
        $this->composerGetter = new ComposerFileGetter($local_adapter);
171✔
558
        if (!$this->composerGetter->hasComposerFile()) {
171✔
559
            throw new \InvalidArgumentException('No composer.json file found.');
1✔
560
        }
561
        $composer_json_data = $this->composerGetter->getComposerJsonData();
170✔
562
        if (false == $composer_json_data) {
170✔
563
            throw new \InvalidArgumentException('Invalid composer.json file');
1✔
564
        }
565
        $config = $this->ensureFreshConfig($composer_json_data);
169✔
566
        $this->runAuthExport($hostname);
169✔
567
        $this->doComposerInstall($config);
169✔
568
        $config = $this->ensureFreshConfig($composer_json_data);
169✔
569
        $this->client = $this->getClient($this->slug);
169✔
570
        $this->privateClient = $this->getClient($this->slug);
169✔
571
        $this->privateClient->authenticate($this->userToken, null);
169✔
572
        if ($is_bitbucket && $bitbucket_user) {
168✔
573
            $this->privateClient->authenticate($bitbucket_user, $this->userToken);
1✔
574
        }
575

576
        $this->logger->log('info', new Message('Checking private status of repo', Message::COMMAND));
168✔
577
        $this->isPrivate = $this->checkPrivateStatus();
168✔
578
        $this->logger->log('info', new Message('Checking default branch of repo', Message::COMMAND));
168✔
579
        $default_branch = $this->checkDefaultBranch();
168✔
580

581
        if ($default_branch) {
168✔
582
            $this->log('Default branch set in project is ' . $default_branch);
164✔
583
        }
584
        // We also allow the project to override this for violinist.
585
        if ($config->getDefaultBranch()) {
168✔
586
            // @todo: Would be better to make sure this can actually be set, based on the branches available. Either
587
            // way, if a person configures this wrong, several parts will fail spectacularly anyway.
588
            $default_branch = $config->getDefaultBranch();
4✔
589
            $this->log('Default branch overridden by config and set to ' . $default_branch);
4✔
590
        }
591
        // Now make sure we are actually on that branch.
592
        if ($this->execCommand(['git', 'remote', 'set-branches', 'origin', "*"])) {
168✔
593
            // We had a problem.
594
            $this->log($this->getLastStdOut());
×
595
            $this->log($this->getLastStdErr());
×
596
            throw new \Exception('There was an error trying to configure default branch');
×
597
        }
598
        if ($this->execCommand(['git', 'fetch', 'origin', $default_branch])) {
168✔
599
            // We had a problem.
600
            $this->log($this->getLastStdOut());
×
601
            $this->log($this->getLastStdErr());
×
602
            throw new \Exception('There was an error trying to fetch default branch');
×
603
        }
604
        if ($this->execCommand(['git', 'checkout', $default_branch])) {
168✔
605
            // We had a problem.
606
            $this->log($this->getLastStdOut());
×
607
            $this->log($this->getLastStdErr());
×
608
            throw new \Exception('There was an error trying to switch to default branch');
×
609
        }
610
        // Re-read the composer.json file, since it can be different on the default branch.
611
        $this->doComposerInstall($config);
168✔
612
        $composer_json_data = $this->composerGetter->getComposerJsonData();
168✔
613
        $config = $this->ensureFreshConfig($composer_json_data);
168✔
614
        $this->runAuthExport($hostname);
168✔
615
        $this->handleDrupalContribSa($composer_json_data);
168✔
616
        $this->handleTimeIntervalSetting($config);
168✔
617
        $lock_file = $this->composerJsonDir . '/composer.lock';
166✔
618
        $initial_composer_lock_data = false;
166✔
619
        $security_alerts = [];
166✔
620
        if (@file_exists($lock_file)) {
166✔
621
            // We might want to know whats in here.
622
            $initial_composer_lock_data = json_decode(file_get_contents($lock_file));
146✔
623
        }
624
        $this->lockFileContents = $initial_composer_lock_data;
166✔
625
        if ($config->shouldAlwaysUpdateAll() && !$initial_composer_lock_data) {
166✔
626
            $this->log('Update all enabled, but no lock file present. This is not supported');
1✔
627
            $this->cleanUp();
1✔
628
            return;
1✔
629
        }
630
        $this->doComposerInstall($config);
165✔
631
        // Now read the lockfile.
632
        $composer_lock_after_installing = json_decode(@file_get_contents($this->composerJsonDir . '/composer.lock'));
165✔
633
        // And do a quick security check in there as well.
634
        try {
635
            $this->log('Checking for security issues in project.');
165✔
636
            $checker = $this->checkerFactory->getChecker();
165✔
637
            $result = $checker->checkDirectory($this->composerJsonDir);
165✔
638
            // Make sure this is an array now.
639
            if (!$result) {
163✔
640
                $result = [];
152✔
641
            }
642
            $this->log('Found ' . count($result) . ' security advisories for packages installed', 'message', [
163✔
643
                'result' => $result,
163✔
644
            ]);
163✔
645
            foreach ($result as $name => $value) {
163✔
646
                $this->log("Security update available for $name");
11✔
647
            }
648
            if (count($result)) {
163✔
649
                $security_alerts = $result;
163✔
650
            }
651
        } catch (\Exception $e) {
2✔
652
            $this->log('Caught exception while looking for security updates:');
2✔
653
            $this->log($e->getMessage());
2✔
654
        }
655
        // We also want to consult the Drupal security advisories, since the FriendsOfPHP/security-advisories
656
        // repo is a manual job merging and maintaining. On top of that, it requires the built container to be
657
        // up to date. So here could be several hours of delay on critical stuff.
658
        $this->attachDrupalAdvisories($security_alerts);
165✔
659
        $direct = null;
165✔
660
        if ($config->shouldCheckDirectOnly()) {
165✔
661
            $this->log('Checking only direct dependencies since config option check_only_direct_dependencies is enabled');
160✔
662
            $direct = '--direct';
160✔
663
        }
664
        // If we should always update all, then of course we should not only check direct dependencies outdated.
665
        // Regardless of the option above actually.
666
        if ($config->shouldAlwaysUpdateAll()) {
165✔
667
            $this->log('Checking all (not only direct dependencies) since config option always_update_all is enabled');
21✔
668
            $direct = null;
21✔
669
        }
670
        // If we should allow indirect packages to updated via running composer update my/direct, then we need to
671
        // uncover which indirect are actually out of date. Meaning direct is required to be false.
672
        if ($config->shouldUpdateIndirectWithDirect()) {
165✔
673
            $this->log('Checking all (not only direct dependencies) since config option allow_update_indirect_with_direct is enabled');
6✔
674
            $direct = null;
6✔
675
        }
676
        $composer_outdated_command = [
165✔
677
            'composer',
165✔
678
            'outdated',
165✔
679
            '--format=json',
165✔
680
            '--no-interaction',
165✔
681
        ];
165✔
682
        if ($direct) {
165✔
683
            $composer_outdated_command[] = $direct;
133✔
684
        }
685
        switch ($config->getComposerOutdatedFlag()) {
165✔
686
            case 'patch':
165✔
687
                $composer_outdated_command[] = '--patch-only';
2✔
688
                break;
2✔
689
            default:
690
                $composer_outdated_command[] = '--minor-only';
163✔
691
                break;
163✔
692
        }
693
        $this->execCommand($composer_outdated_command);
165✔
694
        $raw_data = $this->getLastStdOut();
165✔
695
        $json_update = @json_decode($raw_data);
165✔
696
        if (!$json_update) {
165✔
697
            // We had a problem.
698
            $this->log($this->getLastStdOut());
×
699
            $this->log($this->getLastStdErr());
×
700
            throw new \Exception('The output for available updates could not be parsed as JSON');
×
701
        }
702
        if (!isset($json_update->installed)) {
165✔
703
            // We had a problem.
704
            $this->log($this->getLastStdOut());
2✔
705
            $this->log($this->getLastStdErr());
2✔
706
            throw new \Exception(
2✔
707
                'JSON output from composer was not looking as expected after checking updates'
2✔
708
            );
2✔
709
        }
710
        $data = $json_update->installed;
163✔
711
        if (!is_array($data)) {
163✔
712
            $this->log('Update data was in wrong format or missing. This is an error in violinist and should be reported');
1✔
713
            $this->log(print_r($raw_data, true), Message::COMMAND, [
1✔
714
              'data' => $raw_data,
1✔
715
              'data_guessed' => $data,
1✔
716
            ]);
1✔
717
            $this->cleanUp();
1✔
718
            return;
1✔
719
        }
720
        // Only update the ones in the allow list, if indicated.
721
        $handler = AllowListHandler::createFromConfig($config);
162✔
722
        // If we have an allow list, we should also make sure to include the
723
        // direct ones in it, if indicated.
724
        if ($config->getAllowList() && $config->shouldAlwaysAllowDirect()) {
162✔
725
            $require_list = [];
2✔
726
            if (!empty($composer_json_data->require)) {
2✔
727
                $require_list = array_keys(get_object_vars($composer_json_data->require));
1✔
728
            }
729
            if (!empty($composer_json_data->{'require-dev'})) {
2✔
730
                $require_list = array_merge($require_list, array_keys(get_object_vars($composer_json_data->{'require-dev'})));
1✔
731
            }
732
            $handler = AllowListHandler::createFromArray(array_merge($require_list, $config->getAllowList()));
2✔
733
        }
734
        $handler->setLogger($this->getLogger());
162✔
735
        $data = $handler->applyToItems($data);
162✔
736
        // Remove non-security packages, if indicated.
737
        if ($config->shouldOnlyUpdateSecurityUpdates()) {
162✔
738
            $this->log('Project indicated that it should only receive security updates. Removing non-security related updates from queue');
2✔
739
            foreach ($data as $delta => $item) {
2✔
740
                try {
741
                    $package_name_in_composer_json = Helpers::getComposerJsonName($composer_json_data, $item->name, $this->composerJsonDir);
2✔
742
                    if (isset($security_alerts[$package_name_in_composer_json])) {
2✔
743
                        continue;
2✔
744
                    }
745
                } catch (\Exception $e) {
×
746
                    // Totally fine. Let's check if it's there just by the name we have right here.
747
                    if (isset($security_alerts[$item->name])) {
×
748
                        continue;
×
749
                    }
750
                }
751
                unset($data[$delta]);
1✔
752
                $this->log(sprintf('Skipping update of %s because it is not indicated as a security update', $item->name));
1✔
753
            }
754
        }
755
        // Remove block listed packages.
756
        $block_list = $config->getBlockList();
162✔
757
        if (!is_array($block_list)) {
162✔
758
                $this->log('The format for the package block list was not correct. Expected an array, got ' . gettype($composer_json_data->extra->violinist->blacklist), Message::VIOLINIST_ERROR);
×
759
        } else {
760
            foreach ($data as $delta => $item) {
162✔
761
                if (in_array($item->name, $block_list)) {
160✔
762
                    $this->log(sprintf('Skipping update of %s because it is on the block list', $item->name), Message::BLACKLISTED, [
4✔
763
                        'package' => $item->name,
4✔
764
                    ]);
4✔
765
                    unset($data[$delta]);
4✔
766
                    continue;
4✔
767
                }
768
                // Also try to match on wildcards.
769
                foreach ($block_list as $block_list_item) {
156✔
770
                    if (fnmatch($block_list_item, $item->name)) {
5✔
771
                        $this->log(sprintf('Skipping update of %s because it is on the block list by pattern %s', $item->name, $block_list_item), Message::BLACKLISTED, [
4✔
772
                            'package' => $item->name,
4✔
773
                        ]);
4✔
774
                        unset($data[$delta]);
4✔
775
                        continue 2;
4✔
776
                    }
777
                }
778
            }
779
        }
780
        // Remove dev dependencies, if indicated.
781
        if (!$config->shouldUpdateDevDependencies()) {
162✔
782
            $this->log('Removing dev dependencies from updates since the option update_dev_dependencies is disabled');
3✔
783
            $filterer = DevDepsOnlyFilterer::create($composer_lock_after_installing, $composer_json_data);
3✔
784
            $data = $filterer->filter($data);
3✔
785
        }
786
        foreach ($data as $delta => $item) {
162✔
787
            // Also unset those that are in an unexpected format. A new thing seen in the wild has been this:
788
            // {
789
            //    "name": "symfony/css-selector",
790
            //    "version": "v2.8.49",
791
            //    "description": "Symfony CssSelector Component"
792
            // }
793
            // They should ideally include a latest version and latest status.
794
            if (!isset($item->latest) || !isset($item->{'latest-status'})) {
150✔
795
                unset($data[$delta]);
×
796
            } else {
797
                // If a package is abandoned, we do not really want to know. Since we can't update it anyway.
798
                if (isset($item->version) && ($item->latest === $item->version || $item->{'latest-status'} === 'up-to-date')) {
150✔
799
                    unset($data[$delta]);
3✔
800
                }
801
            }
802
        }
803
        if (empty($data)) {
162✔
804
            $this->log('No updates found');
14✔
805
            $this->cleanUp();
14✔
806
            return;
14✔
807
        }
808
        // Try to log what updates are found.
809
        $this->log('The following updates were found:');
148✔
810
        $updates_string = '';
148✔
811
        foreach ($data as $delta => $item) {
148✔
812
            $updates_string .= sprintf(
148✔
813
                "%s: %s installed, %s available (type %s)\n",
148✔
814
                $item->name,
148✔
815
                $item->version,
148✔
816
                $item->latest,
148✔
817
                $item->{'latest-status'}
148✔
818
            );
148✔
819
        }
820
        $this->log($updates_string, Message::UPDATE, [
148✔
821
            'packages' => $data,
148✔
822
        ]);
148✔
823
        // Try to see if we have already dealt with this (i.e already have a branch for all the updates.
824
        $branch_user = $this->forkUser;
148✔
825
        if ($this->isPrivate) {
148✔
826
            $branch_user = $user_name;
147✔
827
        }
828
        $branch_slug = new Slug();
148✔
829
        $branch_slug->setProvider('github.com');
148✔
830
        $branch_slug->setUserName($branch_user);
148✔
831
        $branch_slug->setUserRepo($user_repo);
148✔
832
        $branches_flattened = [];
148✔
833
        $prs_named = [];
148✔
834
        $default_base = null;
148✔
835
        try {
836
            if ($default_base_upstream = $this->privateClient->getDefaultBase($this->slug, $default_branch)) {
148✔
837
                $default_base = $default_base_upstream;
145✔
838
            }
839
            $prs_named = $this->privateClient->getPrsNamed($this->slug);
148✔
840
            // These can fail if we have not yet created a fork, and the repo is public. That is why we have them at the
841
            // end of this try/catch, so we can still know the default base for the original repo, and its pull
842
            // requests.
843
            if (!$default_base) {
148✔
844
                $default_base = $this->getPrClient()->getDefaultBase($branch_slug, $default_branch);
3✔
845
            }
846
            $branches_flattened = $this->getPrClient()->getBranchesFlattened($branch_slug);
148✔
847
        } catch (RuntimeException $e) {
×
848
            // Safe to ignore.
849
            $this->log('Had a runtime exception with the fetching of branches and Prs: ' . $e->getMessage());
×
850
        }
851
        if ($default_base && $default_branch) {
148✔
852
            $this->log(sprintf('Current commit SHA for %s is %s', $default_branch, $default_base));
145✔
853
        }
854
        $is_allowed_out_of_date_pr = [];
148✔
855
        $one_pr_per_dependency = $config->shouldUseOnePullRequestPerPackage();
148✔
856
        foreach ($data as $delta => $item) {
148✔
857
            $branch_name = Helpers::createBranchName($item, $one_pr_per_dependency, $config);
148✔
858
            if (in_array($branch_name, $branches_flattened)) {
148✔
859
                // Is there a PR for this?
860
                if (array_key_exists($branch_name, $prs_named)) {
26✔
861
                    $this->countPR($item->name);
23✔
862
                    if (!$default_base && !$one_pr_per_dependency) {
23✔
863
                        $this->log(sprintf('Skipping %s because a pull request already exists', $item->name), Message::PR_EXISTS, [
1✔
864
                            'package' => $item->name,
1✔
865
                        ]);
1✔
866
                        unset($data[$delta]);
1✔
867
                        $this->closeOutdatedPrsForPackage($item->name, $item->version, $config, $prs_named[$branch_name]['number'], $prs_named, $default_branch);
1✔
868
                    }
869
                    // Is the pr up to date?
870
                    if ($prs_named[$branch_name]['base']['sha'] == $default_base) {
23✔
871
                        // Create a fake "post-update-data" object.
872
                        $fake_post_update = (object) [
8✔
873
                            'version' => $item->latest,
8✔
874
                        ];
8✔
875
                        $security_update = false;
8✔
876
                        $package_name_in_composer_json = $item->name;
8✔
877
                        try {
878
                            $package_name_in_composer_json = Helpers::getComposerJsonName($composer_json_data, $item->name, $this->composerJsonDir);
8✔
879
                        } catch (\Exception $e) {
×
880
                            // If this was a package that we somehow got because we have allowed to update other than direct
881
                            // dependencies we can avoid re-throwing this.
882
                            if ($config->shouldCheckDirectOnly()) {
×
883
                                throw $e;
×
884
                            }
885
                            // Taking a risk :o.
886
                            $package_name_in_composer_json = $item->name;
×
887
                        }
888
                        if (isset($security_alerts[$package_name_in_composer_json])) {
8✔
889
                            $security_update = true;
3✔
890
                        }
891
                        // If the title does not match, it means either has there arrived a security issue for the
892
                        // update (new title), or we are doing "one-per-dependency", and the title should be something
893
                        // else with this new update. Either way, we want to continue this. Continue in this context
894
                        // would mean, we want to keep this for update checking still, and not unset it from the update
895
                        // array. This will mean it will probably get an updated title later.
896
                        if ($prs_named[$branch_name]['title'] != $this->getPrParamsCreator()->createTitle($item, $fake_post_update, $security_update)) {
8✔
897
                            $this->log(sprintf('Updating the PR of %s since the computed title does not match the title.', $item->name), Message::MESSAGE);
3✔
898
                            continue;
3✔
899
                        }
900
                        $context = [
5✔
901
                            'package' => $item->name,
5✔
902
                        ];
5✔
903
                        if (!empty($prs_named[$branch_name]['html_url'])) {
5✔
904
                            $context['url'] = $prs_named[$branch_name]['html_url'];
×
905
                        }
906
                        $this->log(sprintf('Skipping %s because a pull request already exists', $item->name), Message::PR_EXISTS, $context);
5✔
907
                        $this->closeOutdatedPrsForPackage($item->name, $item->version, $config, $prs_named[$branch_name]['number'], $prs_named, $default_branch);
5✔
908
                        unset($data[$delta]);
5✔
909
                    } else {
910
                        $is_allowed_out_of_date_pr[] = $item->name;
15✔
911
                    }
912
                }
913
            }
914
        }
915
        if ($config->shouldUpdateIndirectWithDirect()) {
148✔
916
            $this->log('Config suggested we should update indirect with direct. Altering the update data based on this');
6✔
917
            $filterer = IndirectWithDirectFilterer::create($composer_lock_after_installing, $composer_json_data);
6✔
918
            $data = $filterer->filter($data);
6✔
919
        }
920
        if (!count($data)) {
148✔
921
            $this->log('No updates that have not already been pushed.');
3✔
922
            $this->cleanUp();
3✔
923
            return;
3✔
924
        }
925

926
        // Unshallow the repo, for syncing it.
927
        $this->execCommand(['git', 'pull', '--unshallow'], false, 600);
145✔
928
        // If the repo is private, we need to push directly to the repo.
929
        if (!$this->isPrivate) {
145✔
930
            $this->preparePrClient();
1✔
931
            $this->log('Creating fork to ' . $this->forkUser);
1✔
932
            $this->client->createFork($user_name, $user_repo, $this->forkUser);
1✔
933
        }
934
        $update_type = self::UPDATE_INDIVIDUAL;
145✔
935
        if ($config->shouldAlwaysUpdateAll()) {
145✔
936
            $update_type = self::UPDATE_ALL;
20✔
937
        }
938
        $this->log('Config suggested update type ' . $update_type);
145✔
939
        if ($this->project && $this->project->shouldUpdateAll()) {
145✔
940
            // Only log this if this might end up being surprising. I mean override all with all. So what?
941
            if ($update_type === self::UPDATE_INDIVIDUAL) {
×
942
                $this->log('Override of update type from project data. Probably meaning first run, allowed update all');
×
943
            }
944
            $update_type = self::UPDATE_ALL;
×
945
        }
946
        switch ($update_type) {
947
            case self::UPDATE_INDIVIDUAL:
145✔
948
                $updater = new IndividualUpdater();
125✔
949
                $updater->setLogger($this->logger);
125✔
950
                $updater->setCWD($this->getCwd());
125✔
951
                $updater->setExecuter($this->executer);
125✔
952
                $updater->setPrCounter($this->getPrCounter());
125✔
953
                $updater->setComposerJsonDir($this->composerJsonDir);
125✔
954
                $updater->setMessageFactory($this->messageFactory);
125✔
955
                $updater->setClient($this->getPrClient());
125✔
956
                $updater->setIsPrivate($this->isPrivate);
125✔
957
                $updater->setSlug($this->slug);
125✔
958
                $updater->setAuthentication($this->untouchedUserToken);
125✔
959
                $updater->setAssigneesAllowed($this->assigneesAllowed);
125✔
960
                if ($this->forkUser) {
125✔
961
                    $updater->setForkUser($this->forkUser);
1✔
962
                }
963
                $updater->setTmpDir($this->tmpDir);
125✔
964
                if ($this->project) {
125✔
965
                    $updater->setProjectData($this->project);
125✔
966
                }
967
                $updater->handleUpdate($data, $composer_lock_after_installing, $composer_json_data, $one_pr_per_dependency, $initial_composer_lock_data, $prs_named, $default_base, $hostname, $default_branch, $security_alerts, $is_allowed_out_of_date_pr, $config);
125✔
968
                break;
125✔
969

970
            case self::UPDATE_ALL:
20✔
971
                $this->handleUpdateAll($initial_composer_lock_data, $composer_lock_after_installing, $security_alerts, $config, $default_base, $default_branch, $prs_named);
20✔
972
                break;
20✔
973
        }
974
        // Clean up.
975
        $this->cleanUp();
145✔
976
    }
977

978
    protected function getPrParamsCreator()
979
    {
980
        if (!$this->prParamsCreator instanceof PrParamsCreator) {
28✔
981
            $this->prParamsCreator = new PrParamsCreator($this->messageFactory, $this->project);
28✔
982
        }
983
        return $this->prParamsCreator;
28✔
984
    }
985

986
    protected function ensureFreshConfig(\stdClass $composer_json_data) : Config
987
    {
988
        return Config::createFromComposerDataInPath($composer_json_data, sprintf('%s/%s', $this->composerJsonDir, 'composer.json'));
169✔
989
    }
990

991
    protected function handleUpdateAll($initial_composer_lock_data, $composer_lock_after_installing, $alerts, Config $config, $default_base, $default_branch, $prs_named)
992
    {
993
        // We are going to hack an item here. We want the package to be "all" and the versions to be blank.
994
        $item = (object) [
20✔
995
            'name' => 'violinist-all',
20✔
996
            'version' => '',
20✔
997
            'latest' => '',
20✔
998
        ];
20✔
999
        $branch_name = Helpers::createBranchName($item, false, $config);
20✔
1000
        $pr_params = [];
20✔
1001
        $security_update = false;
20✔
1002
        try {
1003
            $this->switchBranch($branch_name);
20✔
1004
            $status = $this->execCommand(['composer', 'update']);
20✔
1005
            if ($status) {
20✔
1006
                throw new NotUpdatedException('Composer update command exited with status code ' . $status);
×
1007
            }
1008
            // Now let's find out what has actually been updated.
1009
            $new_lock_contents = json_decode(file_get_contents($this->composerJsonDir . '/composer.lock'));
20✔
1010
            $comparer = new LockDataComparer($composer_lock_after_installing, $new_lock_contents);
20✔
1011
            $list = $comparer->getUpdateList();
20✔
1012
            if (empty($list)) {
20✔
1013
                // That's too bad. Let's throw an exception for this.
1014
                throw new NotUpdatedException('No updates detected after running composer update');
×
1015
            }
1016
            // Now see if any of the packages updated was in the alerts.
1017
            foreach ($list as $value) {
20✔
1018
                if (empty($alerts[$value->getPackageName()])) {
20✔
1019
                    continue;
16✔
1020
                }
1021
                $security_update = true;
4✔
1022
            }
1023
            $this->log('Successfully ran command composer update for all packages');
20✔
1024
            $title = 'Update all composer dependencies';
20✔
1025
            if ($security_update) {
20✔
1026
                // @todo: Use message factory and package.
1027
                $title = sprintf('[SECURITY] %s', $title);
4✔
1028
            }
1029
            // We can do this, since the body creates a title, which it does not use. This is only used for the title.
1030
            // Which, again, we do not use.
1031
            $fake_item = $fake_post = (object) [
20✔
1032
                'name' => 'all',
20✔
1033
                'version' => '0.0.0',
20✔
1034
            ];
20✔
1035
            $body = $this->getPrParamsCreator()->createBody($fake_item, $fake_post, null, $security_update, $list);
20✔
1036
            $pr_params = $this->getPrParamsCreator()->getPrParams($this->forkUser, $this->isPrivate, $this->getSlug(), $branch_name, $body, $title, $default_branch, $config);
20✔
1037
            // OK, so... If we already have a branch named the name we are about to use. Is that one a branch
1038
            // containing all the updates we now got? And is it actually up to date with the target branch? Of course,
1039
            // if there is no such branch, then we will happily push it.
1040
            if (!empty($prs_named[$branch_name])) {
20✔
1041
                $up_to_date = false;
10✔
1042
                if (!empty($prs_named[$branch_name]['base']['sha']) && $prs_named[$branch_name]['base']['sha'] == $default_base) {
10✔
1043
                    $up_to_date = true;
3✔
1044
                }
1045
                $should_update = Helpers::shouldUpdatePr($branch_name, $pr_params, $prs_named);
10✔
1046
                if (!$should_update && $up_to_date) {
10✔
1047
                    // Well well well. Let's not push this branch over and over, shall we?
1048
                    $this->log(sprintf('The branch %s with all updates is already up to date. Aborting the PR update', $branch_name));
1✔
1049
                    return;
1✔
1050
                }
1051
            }
1052
            $this->commitFilesForAll($config);
19✔
1053
            $this->pushCode($branch_name, $default_base, $initial_composer_lock_data);
19✔
1054
            $pullRequest = $this->createPullrequest($pr_params);
19✔
1055
            if (!empty($pullRequest['html_url'])) {
14✔
1056
                $this->log($pullRequest['html_url'], Message::PR_URL, [
4✔
1057
                    'package' => 'all',
4✔
1058
                ]);
4✔
1059
                $this->handleAutomerge($config, $pullRequest, $security_update);
14✔
1060
            }
1061
        } catch (ValidationFailedException $e) {
5✔
1062
            // @todo: Do some better checking. Could be several things, this.
1063
            $this->handlePossibleUpdatePrScenario($e, $branch_name, $pr_params, $prs_named, $config, $security_update);
4✔
1064
        } catch (\Gitlab\Exception\RuntimeException $e) {
1✔
1065
            $this->handlePossibleUpdatePrScenario($e, $branch_name, $pr_params, $prs_named, $config, $security_update);
1✔
1066
        } catch (NotUpdatedException $e) {
×
1067
            $this->log($this->getLastStdOut());
×
1068
            $this->log($this->getLastStdErr());
×
1069
            $not_updated_context = [
×
1070
                'package' => sprintf('all:%s', $default_base),
×
1071
            ];
×
1072
            $this->log("Could not update all dependencies with composer update", Message::NOT_UPDATED, $not_updated_context);
×
1073
        } catch (\Throwable $e) {
×
1074
            $this->log('Caught exception while running update all: ' . $e->getMessage());
×
1075
        }
1076
    }
1077

1078
    protected function commitFilesForAll(Config $config)
1079
    {
1080
        $this->cleanRepoForCommit();
19✔
1081
        $creator = $this->getCommitCreator($config);
19✔
1082
        $msg = $creator->generateMessageFromString('Update all dependencies');
19✔
1083
        $this->commitFiles($msg);
19✔
1084
    }
1085

1086
    protected function handlePossibleUpdatePrScenario(\Exception $e, $branch_name, $pr_params, $prs_named, Config $config, $security_update = false)
1087
    {
1088
        $this->log('Had a problem with creating the pull request: ' . $e->getMessage(), 'error');
5✔
1089
        if (Helpers::shouldUpdatePr($branch_name, $pr_params, $prs_named)) {
5✔
1090
            $this->log('Will try to update the PR based on settings.');
5✔
1091
            $this->getPrClient()->updatePullRequest($this->slug, $prs_named[$branch_name]['number'], $pr_params);
5✔
1092
        }
1093
        if (!empty($prs_named[$branch_name])) {
5✔
1094
            $this->handleAutoMerge($config, $prs_named[$branch_name], $security_update);
5✔
1095
            $this->handleLabels($config, $prs_named[$branch_name], $security_update);
5✔
1096
        }
1097
    }
1098

1099
    protected function handleLabels(Config $config, $pullRequest, $security_update = false) : void
1100
    {
1101
        $labels_allowed = false;
5✔
1102
        $labels_allowed_roles = [
5✔
1103
            'agency',
5✔
1104
            'enterprise',
5✔
1105
        ];
5✔
1106
        if ($this->project && $this->project->getRoles()) {
5✔
1107
            foreach ($this->project->getRoles() as $role) {
2✔
1108
                if (in_array($role, $labels_allowed_roles)) {
2✔
1109
                    $labels_allowed = true;
2✔
1110
                }
1111
            }
1112
        }
1113
        if (!$labels_allowed) {
5✔
1114
            return;
3✔
1115
        }
1116
        Helpers::handleLabels($this->getPrClient(), $this->getLogger(), $this->slug, $config, $pullRequest, $security_update);
2✔
1117
    }
1118

1119
    protected function handleAutoMerge(Config $config, $pullRequest, $security_update = false) : void
1120
    {
1121
        Helpers::handleAutoMerge($this->getPrClient(), $this->getLogger(), $this->slug, $config, $pullRequest, $security_update);
9✔
1122
    }
1123

1124
    /**
1125
     * Get the messages that are logged.
1126
     *
1127
     * @return \eiriksm\CosyComposer\Message[]
1128
     *   The logged messages.
1129
     */
1130
    public function getOutput()
1131
    {
1132
        $msgs = [];
106✔
1133
        if (!$this->logger instanceof ArrayLogger) {
106✔
1134
            return $msgs;
×
1135
        }
1136
        /** @var ArrayLogger $my_logger */
1137
        $my_logger = $this->logger;
106✔
1138
        foreach ($my_logger->get() as $message) {
106✔
1139
            $msg = $message['message'];
106✔
1140
            if (!$msg instanceof Message && is_string($msg)) {
106✔
1141
                $msg = new Message($msg);
7✔
1142
            }
1143
            $msg->setContext($message['context']);
106✔
1144
            if (isset($message['context']['command'])) {
106✔
1145
                $msg = new Message($msg->getMessage(), Message::COMMAND);
80✔
1146
                $msg->setContext($message['context']);
80✔
1147
            }
1148
            $msgs[] = $msg;
106✔
1149
        }
1150
        return $msgs;
106✔
1151
    }
1152

1153
    /**
1154
     * Cleans up after the run.
1155
     */
1156
    private function cleanUp()
1157
    {
1158
        // Run composer install again, so we can get rid of newly installed updates for next run.
1159
        $this->execCommand(['composer', 'install', '--no-ansi', '-n'], false, 1200);
164✔
1160
        $this->chdir('/tmp');
164✔
1161
        $this->log('Cleaning up after update check.');
164✔
1162
        $this->execCommand(['rm', '-rf', $this->tmpDir], false, 300);
164✔
1163
        if (file_exists('/usr/local/bin/composer.bak')) {
164✔
1164
            rename('/usr/local/bin/composer.bak', '/usr/local/bin/composer');
×
1165
        }
1166
    }
1167

1168
    /**
1169
     * Executes a command.
1170
     */
1171
    protected function execCommand(array $command, $log = true, $timeout = 120, $env = [])
1172
    {
1173
        $this->executer->setCwd($this->getCwd());
172✔
1174
        return $this->executer->executeCommand($command, $log, $timeout, $env);
172✔
1175
    }
1176

1177
    /**
1178
     * Log a message.
1179
     *
1180
     * @param string $message
1181
     */
1182
    protected function log($message, $type = 'message', $context = [])
1183
    {
1184

1185
        $this->getLogger()->log('info', new Message($message, $type), $context);
172✔
1186
    }
1187

1188
    protected function attachDrupalAdvisories(array &$alerts)
1189
    {
1190
        // Also though. If the only alert we have is for the package with
1191
        // literally "drupal/core" we need to make sure it's attached to the
1192
        // other names as well.
1193
        $known_names = [
165✔
1194
            'drupal/core-recommended',
165✔
1195
            'drupal/core-composer-scaffold',
165✔
1196
            'drupal/core-project-message',
165✔
1197
            'drupal/core',
165✔
1198
            'drupal/drupal',
165✔
1199
        ];
165✔
1200
        if (!empty($alerts['drupal/core'])) {
165✔
1201
            foreach ($known_names as $known_name) {
4✔
1202
                if (!empty($alerts[$known_name])) {
4✔
1203
                    continue;
4✔
1204
                }
1205
                $alerts[$known_name] = $alerts['drupal/core'];
4✔
1206
            }
1207
        }
1208
        if (!$this->lockFileContents) {
165✔
1209
            return;
19✔
1210
        }
1211
        $data = ComposerLockData::createFromString(json_encode($this->lockFileContents));
146✔
1212
        try {
1213
            $drupal = $data->getPackageData('drupal/core');
146✔
1214
            // Now see if a newer version is available, and if it is a security update.
1215
            $endpoint = 'current';
34✔
1216
            $version_parts = explode('.', $drupal->version);
34✔
1217
            $major_version = $version_parts[0];
34✔
1218
            // Only 7.x and 8.x use their own endpoint,
1219
            if (in_array($major_version, ['7', '8'])) {
34✔
1220
                $endpoint = $major_version . '.x';
9✔
1221
            }
1222
            if ((int) $major_version < 7) {
34✔
1223
                throw new \Exception(sprintf('Drupal version %s is too old to check for security updates using drupal.org endpoint', $major_version));
×
1224
            }
1225
            $client = $this->getHttpClient();
34✔
1226
            $url = sprintf('https://updates.drupal.org/release-history/drupal/%s', $endpoint);
34✔
1227
            $request = new Request('GET', $url);
34✔
1228
            $response = $client->sendRequest($request);
34✔
1229
            $data = $response->getBody()->getContents();
34✔
1230
            $xml = @simplexml_load_string($data);
34✔
1231
            if (!$xml) {
34✔
1232
                return;
×
1233
            }
1234
            if (empty($xml->releases->release)) {
34✔
1235
                return;
17✔
1236
            }
1237
            $drupal_version_array = explode('.', $drupal->version);
17✔
1238
            $active_branch = sprintf('%s.%s', $drupal_version_array[0], $drupal_version_array[1]);
17✔
1239
            $supported_branches = explode(',', (string) $xml->supported_branches);
17✔
1240
            $is_supported = false;
17✔
1241
            foreach ($supported_branches as $branch) {
17✔
1242
                if (strpos($branch, $active_branch) === 0) {
17✔
1243
                    $is_supported = true;
7✔
1244
                }
1245
            }
1246
            foreach ($xml->releases->release as $release) {
17✔
1247
                if (empty($release->version)) {
17✔
1248
                    continue;
×
1249
                }
1250
                if (empty($release->terms) || empty($release->terms->term)) {
17✔
1251
                    continue;
8✔
1252
                }
1253
                $version = (string) $release->version;
17✔
1254
                // If they are not on the same branch, then let's skip it as well.
1255
                if ($endpoint !== '7.x') {
17✔
1256
                    if ($is_supported && strpos($version, $active_branch) !== 0) {
15✔
1257
                        continue;
5✔
1258
                    }
1259
                }
1260
                if (version_compare($version, $drupal->version) !== 1) {
17✔
1261
                    continue;
8✔
1262
                }
1263
                $is_sec = false;
11✔
1264
                foreach ($release->terms->term as $item) {
11✔
1265
                    $type = (string) $item->value;
11✔
1266
                    if ($type === 'Security update') {
11✔
1267
                        $is_sec = true;
11✔
1268
                    }
1269
                }
1270
                if (!$is_sec) {
11✔
1271
                    continue;
7✔
1272
                }
1273
                if (strpos($release->version, $major_version) !== 0) {
11✔
1274
                    // You know what. We must be checking version 10.x against
1275
                    // version 9.x. Not ideal, is it? Makes for some false
1276
                    // positives (or rather negatives, I guess).
1277
                    continue;
4✔
1278
                }
1279
                $this->log('Found a security update in the update XML. Will populate advisories from this, if not already set.');
9✔
1280
                foreach ($known_names as $known_name) {
9✔
1281
                    if (!empty($alerts[$known_name])) {
9✔
1282
                        continue;
×
1283
                    }
1284
                    $alerts[$known_name] = [
9✔
1285
                        'version' => $version,
9✔
1286
                    ];
9✔
1287
                }
1288
                break;
9✔
1289
            }
1290
        } catch (\Throwable $e) {
112✔
1291
            // Totally fine.
1292
        }
1293
    }
1294

1295
   /**
1296
    * Changes to a different directory.
1297
    */
1298
    private function chdir($dir)
1299
    {
1300
        if (!file_exists($dir)) {
171✔
UNCOV
1301
            return false;
×
1302
        }
1303
        $this->setCWD($dir);
171✔
1304
        return true;
171✔
1305
    }
1306

1307
    protected function setCWD($dir)
1308
    {
1309
        $this->cwd = $dir;
171✔
1310
    }
1311

1312

1313
    /**
1314
     * @return string
1315
     */
1316
    public function getTmpDir()
1317
    {
1318
        return $this->tmpDir;
×
1319
    }
1320

1321
    /**
1322
     * @param Slug $slug
1323
     *
1324
     * @return ProviderInterface
1325
     */
1326
    private function getClient(Slug $slug)
1327
    {
1328
        if (!$this->providerFactory instanceof ProviderFactory) {
169✔
1329
            $this->setProviderFactory(new ProviderFactory());
×
1330
        }
1331
        return $this->providerFactory->createFromHost($slug, $this->urlArray);
169✔
1332
    }
1333

1334
    /**
1335
     * Get the client we should use for the PRs we create.
1336
     */
1337
    private function getPrClient() : ProviderInterface
1338
    {
1339
        if ($this->isPrivate) {
148✔
1340
            return $this->privateClient;
147✔
1341
        }
1342
        $this->preparePrClient();
1✔
1343
        $this->client->authenticate($this->userToken, null);
1✔
1344
        return $this->client;
1✔
1345
    }
1346

1347
    private function preparePrClient() : void
1348
    {
1349
        // We are only allowed to use the public github wrapper if the magic env
1350
        // for this is set, which it will be in jobs coming from the SaaS
1351
        // offering, but not for self hosted.
1352
        $this->logger->log('info', new Message('Checking if we should enable the public github wrapper', Message::COMMAND));
1✔
1353
        if (!self::shouldEnablePublicGithubWrapper()) {
1✔
1354
            // The client should hopefully be fully prepared.
1355
            $this->logger->log('info', new Message('Public github wrapper not enabled', Message::COMMAND));
×
1356
            return;
×
1357
        }
1358
        if (!$this->isPrivate) {
1✔
1359
            $this->logger->log('info', new Message('Public github wrapper enabled', Message::COMMAND));
1✔
1360
            if (!$this->client instanceof PublicGithubWrapper) {
1✔
1361
                $this->client = new PublicGithubWrapper(new Client());
×
1362
            }
1363
            $this->client->setUserToken($this->userToken);
1✔
1364
            $this->client->setUrlFromTokenUrl($this->tokenUrl);
1✔
1365
            $this->client->setProject($this->project);
1✔
1366
        }
1367
    }
1368

1369
    private function checkDefaultBranch() : ?string
1370
    {
1371
        $default_branch = null;
168✔
1372
        try {
1373
            $default_branch = $this->privateClient->getDefaultBranch($this->slug);
168✔
1374
        } catch (\Throwable $e) {
×
1375
            // Could be a personal access token.
1376
            if (!method_exists($this->privateClient, 'authenticatePersonalAccessToken')) {
×
1377
                throw $e;
×
1378
            }
1379
            try {
1380
                $this->privateClient->authenticatePersonalAccessToken($this->userToken, null);
×
1381
                $default_branch = $this->privateClient->getDefaultBranch($this->slug);
×
1382
            } catch (\Throwable $other_exception) {
×
1383
                throw $e;
×
1384
            }
1385
        }
1386
        return $default_branch;
168✔
1387
    }
1388

1389
    private function checkPrivateStatus() : bool
1390
    {
1391
        if (!self::shouldEnablePublicGithubWrapper()) {
168✔
1392
            return true;
167✔
1393
        }
1394
        try {
1395
            return $this->privateClient->repoIsPrivate($this->slug);
1✔
1396
        } catch (\Throwable $e) {
×
1397
            // Could be a personal access token.
1398
            if (!method_exists($this->privateClient, 'authenticatePersonalAccessToken')) {
×
1399
                return true;
×
1400
            }
1401
            try {
1402
                $this->privateClient->authenticatePersonalAccessToken($this->userToken, null);
×
1403
                return $this->privateClient->repoIsPrivate($this->slug);
×
1404
            } catch (\Throwable $other_exception) {
×
1405
                throw $e;
×
1406
            }
1407
        }
1408
    }
1409

1410
    protected function getlockFileContents()
1411
    {
1412
        return $this->lockFileContents;
19✔
1413
    }
1414

1415
    public static function shouldEnablePublicGithubWrapper() : bool
1416
    {
1417
        return !empty(getenv('USE_GITHUB_PUBLIC_WRAPPER'));
173✔
1418
    }
1419
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc