Skip to content

Commit 7a1f47d

Browse files
author
ityaozm@gmail.com
committed
feat(composer-updater): add composer-updater script and configuration
- Added composer-updater script - Created composer-updater configuration in composer.json - Configured composer-updater script with options - Included composer-updater dry-run option
1 parent 8598a2f commit 7a1f47d

File tree

4 files changed

+339
-0
lines changed

4 files changed

+339
-0
lines changed

.php-cs-fixer.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
'vendor/',
3838
])
3939
->append(glob(__DIR__.'/{*.php,.*.php}', GLOB_BRACE))
40+
->append([
41+
__DIR__.'/composer-updater',
42+
])
4043
->notPath([
4144
'bootstrap/*',
4245
'storage/*',

composer-updater

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
declare(strict_types=1);
5+
6+
/**
7+
* This file is part of the guanguans/laravel-exception-notify.
8+
*
9+
* (c) guanguans <ityaozm@gmail.com>
10+
*
11+
* This source file is subject to the MIT license that is bundled.
12+
*/
13+
14+
use Composer\InstalledVersions;
15+
use SebastianBergmann\Diff\Differ;
16+
use Symfony\Component\Console\Input\ArgvInput;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Input\InputOption;
19+
use Symfony\Component\Console\Output\ConsoleOutput;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
use Symfony\Component\Console\SingleCommandApplication;
22+
use Symfony\Component\Console\Style\SymfonyStyle;
23+
use Symfony\Component\Process\ExecutableFinder;
24+
use Symfony\Component\Process\PhpExecutableFinder;
25+
use Symfony\Component\Process\Process;
26+
27+
require __DIR__.'/vendor/autoload.php';
28+
29+
/** @noinspection PhpUnhandledExceptionInspection */
30+
$status = (new SingleCommandApplication())
31+
->setName('Composer Updater')
32+
->addOption('composer-json-path', null, InputOption::VALUE_OPTIONAL)
33+
->addOption('highest-php-binary', null, InputOption::VALUE_REQUIRED)
34+
->addOption('composer-binary', null, InputOption::VALUE_OPTIONAL)
35+
->addOption('except-packages', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL)
36+
->addOption('except-dependency-versions', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL)
37+
->addOption('dry-run', null, InputOption::VALUE_NONE)
38+
->setCode(function (InputInterface $input, OutputInterface $output): void {
39+
assert_options(ASSERT_BAIL, 1);
40+
assert($this instanceof SingleCommandApplication);
41+
assert((bool) $input->getOption('highest-php-binary'));
42+
43+
(new class($input->getOption('composer-json-path') ?: __DIR__.'/composer.json', $input->getOption('highest-php-binary'), $input->getOption('composer-binary'), $input->getOption('except-packages'), $input->getOption('except-dependency-versions'), $input->getOption('dry-run'), new SymfonyStyle($input, $output), new Differ(), ) {
44+
private $composerJsonPath;
45+
46+
private $composerJsonContents;
47+
48+
private $highestComposerBinary;
49+
50+
private $composerBinary;
51+
52+
private $exceptPackages;
53+
54+
private $exceptDependencyVersions;
55+
56+
private $dryRun;
57+
58+
private $symfonyStyle;
59+
60+
private $differ;
61+
62+
/**
63+
* @noinspection ParameterDefaultsNullInspection
64+
*/
65+
public function __construct(
66+
string $composerJsonPath,
67+
?string $highestPhpBinary = null,
68+
?string $composerBinary = null,
69+
array $exceptPackages = [],
70+
array $exceptDependencyVersions = [],
71+
bool $dryRun = false,
72+
?SymfonyStyle $symfonyStyle = null,
73+
?Differ $differ = null
74+
) {
75+
assert_options(ASSERT_BAIL, 1);
76+
assert((bool) $composerJsonPath);
77+
78+
$this->composerJsonPath = $composerJsonPath;
79+
$this->composerJsonContents = file_get_contents($composerJsonPath);
80+
$this->highestComposerBinary = $this->getComposerBinary($composerBinary, $highestPhpBinary);
81+
$this->composerBinary = $this->getComposerBinary($composerBinary);
82+
$this->exceptPackages = array_merge([
83+
'php',
84+
'ext-*',
85+
], $exceptPackages);
86+
$this->exceptDependencyVersions = array_merge([
87+
'\*',
88+
'*-*',
89+
'*@*',
90+
// '*|*',
91+
], $exceptDependencyVersions);
92+
$this->dryRun = $dryRun;
93+
$this->symfonyStyle = $symfonyStyle ?? new SymfonyStyle(new ArgvInput(), new ConsoleOutput());
94+
$this->differ = $differ ?? new Differ();
95+
}
96+
97+
public function __invoke(): void
98+
{
99+
$this
100+
->updateComposerPackages()
101+
->updateOutdatedComposerPackages()
102+
->updateComposerPackages()
103+
->updateOutdatedComposerPackages()
104+
->updateComposerPackages()
105+
->normalizeComposerJson()
106+
->success();
107+
}
108+
109+
private function updateComposerPackages(): self
110+
{
111+
$this->mustRunCommand("$this->composerBinary update -W --ansi");
112+
113+
return $this;
114+
}
115+
116+
/**
117+
* @noinspection JsonEncodingApiUsageInspection
118+
*
119+
* @return $this|never-return
120+
*/
121+
private function updateOutdatedComposerPackages(): self
122+
{
123+
$outdatedComposerJsonContents = json_encode(
124+
$this->getOutdatedDecodedComposerJson(),
125+
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
126+
).PHP_EOL;
127+
128+
if ($this->dryRun) {
129+
$this->symfonyStyle->writeln($this->formatDiff($this->differ->diff(
130+
$this->composerJsonContents,
131+
$outdatedComposerJsonContents
132+
)));
133+
134+
exit(0);
135+
}
136+
137+
file_put_contents($this->composerJsonPath, $outdatedComposerJsonContents);
138+
139+
return $this;
140+
}
141+
142+
private function normalizeComposerJson(): self
143+
{
144+
$this->mustRunCommand("$this->composerBinary normalize --diff --ansi");
145+
146+
return $this;
147+
}
148+
149+
private function success(): void
150+
{
151+
$this->symfonyStyle->success('Composer packages updated successfully!');
152+
}
153+
154+
/**
155+
* @noinspection JsonEncodingApiUsageInspection
156+
*/
157+
private function getOutdatedDecodedComposerJson(): array
158+
{
159+
$outdatedComposerPackages = $this->getOutdatedComposerPackages();
160+
$decodedComposerJson = json_decode(file_get_contents($this->composerJsonPath), true);
161+
(function () {
162+
return self::reload(null);
163+
})->call(new InstalledVersions());
164+
165+
foreach ($decodedComposerJson as $name => &$value) {
166+
if (! in_array($name, ['require', 'require-dev'], true)) {
167+
continue;
168+
}
169+
170+
foreach ($value as $package => &$dependencyVersion) {
171+
if (
172+
$this->strIs($this->exceptPackages, $package)
173+
|| $this->strIs($this->exceptDependencyVersions, $dependencyVersion)
174+
) {
175+
continue;
176+
}
177+
178+
if ($version = InstalledVersions::getVersion($package)) {
179+
$dependencyVersion = $this->toDependencyVersion($version);
180+
}
181+
182+
if (isset($outdatedComposerPackages[$package])) {
183+
$dependencyVersion = $outdatedComposerPackages[$package]['dependency_version'];
184+
}
185+
}
186+
}
187+
188+
return $decodedComposerJson;
189+
}
190+
191+
/**
192+
* @noinspection JsonEncodingApiUsageInspection
193+
*/
194+
private function getOutdatedComposerPackages(): array
195+
{
196+
return array_reduce(
197+
json_decode(
198+
$this
199+
->mustRunCommand("$this->highestComposerBinary outdated --format=json --direct --ansi")
200+
->getOutput(),
201+
true
202+
)['installed'],
203+
function (array $carry, array $package): array {
204+
$lowestArrayVersion = $this->toArrayVersion($package['version']);
205+
$highestArrayVersion = $this->toArrayVersion($package['latest']);
206+
$dependencyVersions = [$this->toDependencyVersion($package['version'])];
207+
208+
if ($lowestArrayVersion[0] !== $highestArrayVersion[0]) {
209+
$dependencyVersions = array_merge($dependencyVersions, array_map(
210+
static function (string $major): string {
211+
return "^$major.0";
212+
},
213+
range($lowestArrayVersion[0] + 1, $highestArrayVersion[0])
214+
));
215+
}
216+
217+
$package['dependency_version'] = implode(' || ', $dependencyVersions);
218+
$carry[$package['name']] = $package;
219+
220+
return $carry;
221+
},
222+
[]
223+
);
224+
}
225+
226+
private function getComposerBinary(?string $composerBinary = null, ?string $phpBinary = null): string
227+
{
228+
return sprintf(
229+
'%s %s',
230+
$phpBinary ?? (new PhpExecutableFinder())->find(),
231+
$composerBinary ?? (new ExecutableFinder())->find('composer')
232+
);
233+
}
234+
235+
/**
236+
* @param array|string $command
237+
* @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input
238+
*
239+
* @noinspection MissingParameterTypeDeclarationInspection
240+
* @noinspection PhpSameParameterValueInspection
241+
*/
242+
private function mustRunCommand(
243+
$command,
244+
?string $cwd = null,
245+
?array $env = null,
246+
$input = null,
247+
?float $timeout = 300
248+
): Process {
249+
$process = is_string($command)
250+
? Process::fromShellCommandline($command, $cwd, $env, $input, $timeout)
251+
: new Process($command, $cwd, $env, $input, $timeout);
252+
253+
$this->symfonyStyle->warning($process->getCommandLine());
254+
255+
return $process
256+
->setWorkingDirectory(dirname($this->composerJsonPath))
257+
->setEnv(['COMPOSER_MEMORY_LIMIT' => -1])
258+
->mustRun(function (string $type, string $buffer): void {
259+
$this->symfonyStyle->isVerbose() and $this->symfonyStyle->write($buffer);
260+
});
261+
}
262+
263+
private function toDependencyVersion(string $version): string
264+
{
265+
return '^'.implode('.', array_slice($this->toArrayVersion($version), 0, 2));
266+
}
267+
268+
private function toArrayVersion(string $version): array
269+
{
270+
return explode('.', ltrim($version, 'v'));
271+
}
272+
273+
/**
274+
* @param array|string $pattern
275+
*
276+
* @noinspection SuspiciousLoopInspection
277+
* @noinspection ComparisonScalarOrderInspection
278+
* @noinspection MissingParameterTypeDeclarationInspection
279+
*/
280+
private function strIs($pattern, string $value): bool
281+
{
282+
$patterns = (array) $pattern;
283+
if (empty($patterns)) {
284+
return false;
285+
}
286+
287+
foreach ($patterns as $pattern) {
288+
$pattern = (string) $pattern;
289+
if ($pattern === $value) {
290+
return true;
291+
}
292+
293+
$pattern = preg_quote($pattern, '#');
294+
$pattern = str_replace('\*', '.*', $pattern);
295+
if (1 === preg_match('#^'.$pattern.'\z#u', $value)) {
296+
return true;
297+
}
298+
}
299+
300+
return false;
301+
}
302+
303+
private function formatDiff(string $diff): string
304+
{
305+
$lines = explode(
306+
"\n",
307+
$diff,
308+
);
309+
310+
$formatted = array_map(static function (string $line): string {
311+
return preg_replace(
312+
[
313+
'/^(\+.*)$/',
314+
'/^(-.*)$/',
315+
],
316+
[
317+
'<fg=green>$1</>',
318+
'<fg=red>$1</>',
319+
],
320+
$line,
321+
);
322+
}, $lines);
323+
324+
return implode(
325+
"\n",
326+
$formatted,
327+
);
328+
}
329+
})();
330+
})
331+
->run();
332+
333+
exit($status);

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@
159159
"composer-bin": "@composer bin --ansi -v",
160160
"composer-check-platform-reqs": "@composer check-platform-reqs --lock --ansi -v",
161161
"composer-unused": "@php ./vendor/bin/composer-unused --ansi -v",
162+
"composer-updater": "@php ./composer-updater --highest-php-binary=/opt/homebrew/opt/php@8.3/bin/php --except-packages=guanguans/notify --except-packages=laravel/lumen-framework --except-packages=orchestra/testbench --except-packages=pestphp/pest-plugin-laravel --ansi",
163+
"composer-updater-dry-run": "@composer-updater --dry-run",
162164
"composer-validate": "@composer validate --strict --ansi -v",
163165
"haulable": "haulable --ansi -v",
164166
"haulable-package": "@haulable package ./builds/ai-commit",

composer.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)