Skip to content

Commit 66169ae

Browse files
committed
Track abandoned and unabandoned events in transparency log
1 parent e628efa commit 66169ae

File tree

13 files changed

+423
-4
lines changed

13 files changed

+423
-4
lines changed

src/Audit/AbandonmentReason.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php declare(strict_types=1);
2+
3+
/*
4+
* This file is part of Packagist.
5+
*
6+
* (c) Jordi Boggiano <[email protected]>
7+
* Nils Adermann <[email protected]>
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
namespace App\Audit;
14+
15+
enum AbandonmentReason: string
16+
{
17+
case Manual = 'manual';
18+
case RepositoryArchived = 'repository_archived';
19+
case ComposerJson = 'composer_json';
20+
case Both = 'both';
21+
case Unknown = 'unknown';
22+
}

src/Audit/AuditRecordType.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ enum AuditRecordType: string
2626
case VersionDeleted = 'version_deleted';
2727

2828
case VersionReferenceChanged = 'version_reference_changed';
29-
case PackageAbandoned = 'package_abandoned'; // TODO
30-
case PackageUnabandoned = 'package_unabandoned'; // TODO
29+
case PackageAbandoned = 'package_abandoned';
30+
case PackageUnabandoned = 'package_unabandoned';
3131

3232
// user management
3333
case UserCreated = 'user_created'; // TODO

src/Audit/Display/AuditLogDisplayFactory.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ public function buildSingle(AuditRecord $record): AuditLogDisplayInterface
5353
$record->attributes['repository_to'],
5454
$this->buildActor($record->attributes['actor']),
5555
),
56+
AuditRecordType::PackageAbandoned => new PackageAbandonedDisplay(
57+
$record->datetime,
58+
$record->attributes['name'],
59+
$record->attributes['repository'],
60+
$record->attributes['replacement_package'] ?? null,
61+
$record->attributes['reason'] ?? null,
62+
$this->buildActor($record->attributes['actor']),
63+
),
64+
AuditRecordType::PackageUnabandoned => new PackageUnabandonedDisplay(
65+
$record->datetime,
66+
$record->attributes['name'],
67+
$record->attributes['repository'],
68+
$record->attributes['previous_replacement_package'] ?? null,
69+
$this->buildActor($record->attributes['actor']),
70+
),
5671
AuditRecordType::VersionDeleted => new VersionDeletedDisplay(
5772
$record->datetime,
5873
$record->attributes['name'],
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types=1);
2+
3+
/*
4+
* This file is part of Packagist.
5+
*
6+
* (c) Jordi Boggiano <[email protected]>
7+
* Nils Adermann <[email protected]>
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
namespace App\Audit\Display;
14+
15+
use App\Audit\AuditRecordType;
16+
17+
readonly class PackageAbandonedDisplay extends AbstractAuditLogDisplay
18+
{
19+
public function __construct(
20+
\DateTimeImmutable $datetime,
21+
public string $packageName,
22+
public string $repository,
23+
public ?string $replacementPackage,
24+
public ?string $reason,
25+
ActorDisplay $actor,
26+
) {
27+
parent::__construct($datetime, $actor);
28+
}
29+
30+
public function getType(): AuditRecordType
31+
{
32+
return AuditRecordType::PackageAbandoned;
33+
}
34+
35+
public function getTemplateName(): string
36+
{
37+
return 'audit_log/display/package_abandoned.html.twig';
38+
}
39+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types=1);
2+
3+
/*
4+
* This file is part of Packagist.
5+
*
6+
* (c) Jordi Boggiano <[email protected]>
7+
* Nils Adermann <[email protected]>
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
namespace App\Audit\Display;
14+
15+
use App\Audit\AuditRecordType;
16+
17+
readonly class PackageUnabandonedDisplay extends AbstractAuditLogDisplay
18+
{
19+
public function __construct(
20+
\DateTimeImmutable $datetime,
21+
public string $packageName,
22+
public string $repository,
23+
public ?string $previousReplacementPackage,
24+
ActorDisplay $actor,
25+
) {
26+
parent::__construct($datetime, $actor);
27+
}
28+
29+
public function getType(): AuditRecordType
30+
{
31+
return AuditRecordType::PackageUnabandoned;
32+
}
33+
34+
public function getTemplateName(): string
35+
{
36+
return 'audit_log/display/package_unabandoned.html.twig';
37+
}
38+
}

src/Entity/AuditRecord.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
namespace App\Entity;
1414

15+
use App\Audit\AbandonmentReason;
1516
use App\Audit\AuditRecordType;
1617
use Doctrine\DBAL\Types\Types;
1718
use Doctrine\ORM\Mapping as ORM;
@@ -109,6 +110,16 @@ public static function maintainerRemoved(Package $package, User $maintainer, ?Us
109110
return new self(AuditRecordType::MaintainerRemoved, ['name' => $package->getName(), 'maintainer' => self::getUserData($maintainer), 'actor' => self::getUserData($actor)], $actor?->getId(), $package->getVendor(), $package->getId(), $maintainer->getId());
110111
}
111112

113+
public static function packageAbandoned(Package $package, ?User $actor, ?string $replacementPackage, ?AbandonmentReason $reason = null): self
114+
{
115+
return new self(AuditRecordType::PackageAbandoned, ['name' => $package->getName(), 'repository' => $package->getRepository(), 'replacement_package' => $replacementPackage, 'reason' => $reason?->value, 'actor' => self::getUserData($actor, 'automation')], $actor?->getId(), $package->getVendor(), $package->getId());
116+
}
117+
118+
public static function packageUnabandoned(Package $package, ?User $actor, ?string $previousReplacementPackage): self
119+
{
120+
return new self(AuditRecordType::PackageUnabandoned, ['name' => $package->getName(), 'repository' => $package->getRepository(), 'previous_replacement_package' => $previousReplacementPackage, 'actor' => self::getUserData($actor, 'automation')], $actor?->getId(), $package->getVendor(), $package->getId());
121+
}
122+
112123
/**
113124
* @return array{id: int, username: string}|string
114125
*/

src/Entity/Package.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
namespace App\Entity;
1414

15+
use App\Audit\AbandonmentReason;
1516
use App\Service\UpdaterWorker;
1617
use App\Util\HttpDownloaderOptionsFactory;
1718
use App\Validator\Copyright;
@@ -206,6 +207,10 @@ class Package
206207
* @internal
207208
*/
208209
public ?string $vcsDriverError = null;
210+
/**
211+
* @internal
212+
*/
213+
public ?AbandonmentReason $abandonmentReason = null;
209214

210215
/**
211216
* @var array<string, Version>|null lookup table for versions

src/EventListener/PackageListener.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
namespace App\EventListener;
1414

15+
use App\Audit\AbandonmentReason;
1516
use App\Entity\AuditRecord;
1617
use App\Entity\Package;
1718
use App\Entity\User;
@@ -64,6 +65,24 @@ public function preUpdate(Package $package, PreUpdateEventArgs $event): void
6465
// buffering things to be inserted in postUpdate once we can confirm it is done
6566
$this->buffered[] = AuditRecord::canonicalUrlChange($package, $this->getUser(), $event->getOldValue('repository'));
6667
}
68+
69+
if ($event->hasChangedField('abandoned')) {
70+
$newValue = $event->getNewValue('abandoned');
71+
if ($newValue === true) {
72+
$reason = $package->abandonmentReason;
73+
74+
if ($this->getUser()) {
75+
$reason = AbandonmentReason::Manual;
76+
} else {
77+
$reason = $reason ?? AbandonmentReason::Unknown;
78+
}
79+
80+
$this->buffered[] = AuditRecord::packageAbandoned($package, $this->getUser(), $package->getReplacementPackage(), $reason);
81+
} else {
82+
$oldReplacementPackage = $event->hasChangedField('replacementPackage') ? $event->getOldValue('replacementPackage') : $package->getReplacementPackage();
83+
$this->buffered[] = AuditRecord::packageUnabandoned($package, $this->getUser(), $oldReplacementPackage);
84+
}
85+
}
6786
}
6887

6988
/**

src/Package/Updater.php

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
namespace App\Package;
1414

15+
use App\Audit\AbandonmentReason;
1516
use App\Entity\ConflictLink;
1617
use App\Entity\Dependent;
1718
use App\Entity\DevRequireLink;
@@ -251,7 +252,7 @@ public function update(IOInterface $io, Config $config, Package $package, VcsRep
251252
}
252253
$processedVersions[strtolower($version->getVersion())] = $version;
253254

254-
$result = $this->updateInformation($io, $versionRepository, $package, $existingVersions, $version, $flags, $rootIdentifier);
255+
$result = $this->updateInformation($io, $versionRepository, $package, $existingVersions, $version, $flags, $rootIdentifier, $driver);
255256
$versionId = false;
256257
if ($result['updated']) {
257258
\assert($result['object'] instanceof Version);
@@ -364,7 +365,7 @@ public function update(IOInterface $io, Config $config, Package $package, VcsRep
364365
*
365366
* @return array{updated: true, id: int|null, version: string, object: Version}|array{updated: false, id: int|null, version: string, object: null}
366367
*/
367-
private function updateInformation(IOInterface $io, VersionRepository $versionRepo, Package $package, array $existingVersions, CompletePackageInterface $data, int $flags, string $rootIdentifier): array
368+
private function updateInformation(IOInterface $io, VersionRepository $versionRepo, Package $package, array $existingVersions, CompletePackageInterface $data, int $flags, string $rootIdentifier, VcsDriverInterface $driver): array
368369
{
369370
$em = $this->getEM();
370371
$version = new Version();
@@ -427,6 +428,7 @@ private function updateInformation(IOInterface $io, VersionRepository $versionRe
427428
$package->setType($this->sanitize($data->getType()));
428429
if ($data->isAbandoned() && !$package->isAbandoned()) {
429430
$io->write('Marking package abandoned as per composer metadata from '.$version->getVersion());
431+
$package->abandonmentReason = $this->detectAbandonmentReason($driver, $rootIdentifier);
430432
$package->setAbandoned(true);
431433
if ($data->getReplacementPackage()) {
432434
$package->setReplacementPackage($data->getReplacementPackage());
@@ -845,4 +847,41 @@ private function sanitize(?string $str): ?string
845847

846848
return Preg::replace("{[\x01-\x1A]}u", '', $str);
847849
}
850+
851+
private function detectAbandonmentReason(VcsDriverInterface $driver, string $rootIdentifier): AbandonmentReason
852+
{
853+
$isArchived = false;
854+
$composerHasAbandoned = false;
855+
856+
// is repository archived (GitHub or GitLab)
857+
if ($driver instanceof GitHubDriver || $driver instanceof GitLabDriver) {
858+
try {
859+
$repoData = $driver->getRepoData();
860+
$isArchived = !empty($repoData['archived']);
861+
} catch (\Exception $e) {
862+
// If we can't get repo data, assume not archived
863+
}
864+
}
865+
866+
// abandoned field in composer.json explicitly set
867+
try {
868+
$composerJson = $driver->getFileContent('composer.json', $rootIdentifier);
869+
if ($composerJson) {
870+
$composerData = json_decode($composerJson, true);
871+
$composerHasAbandoned = isset($composerData['abandoned']);
872+
}
873+
} catch (\Exception $e) {
874+
// composer.json couldn't be read, so the abandoned state couldn't be retrieved
875+
}
876+
877+
if ($isArchived && $composerHasAbandoned) {
878+
return AbandonmentReason::Both;
879+
} elseif ($isArchived) {
880+
return AbandonmentReason::RepositoryArchived;
881+
} elseif ($composerHasAbandoned) {
882+
return AbandonmentReason::ComposerJson;
883+
}
884+
885+
return AbandonmentReason::Unknown;
886+
}
848887
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<strong>
2+
{%- if display.packageName is existing_package -%}
3+
<a href="{{ path('view_package', { 'name': display.packageName }) }}">{{ display.packageName }}</a>
4+
{%- else -%}
5+
{{ display.packageName }}
6+
{%- endif -%}
7+
</strong><br>
8+
Repository: {{ display.repository }}<br>
9+
{%- if display.reason -%}
10+
Reason:
11+
{% if display.reason == 'repository_archived' -%}
12+
Repository archived on GitHub/GitLab
13+
{% elseif display.reason == 'composer_json' -%}
14+
<em>abandoned</em> flag set in composer.json
15+
{% elseif display.reason == 'both' -%}
16+
Repository archived and <em>abandoned</em> flag set in composer.json
17+
{% elseif display.reason == 'manual' -%}
18+
Manually set by maintainer
19+
{% else -%}
20+
{{ display.reason }}
21+
{% endif -%}<br>
22+
{%- endif -%}
23+
{%- if display.replacementPackage -%}
24+
Replacement: {% if display.replacementPackage is existing_package -%}
25+
<a href="{{ path('view_package', { 'name': display.replacementPackage }) }}">{{ display.replacementPackage }}</a>
26+
{%- else -%}
27+
{{ display.replacementPackage }}
28+
{%- endif -%}<br>
29+
{%- endif -%}
30+
Abandoned by: {{ display.actor.username }}

0 commit comments

Comments
 (0)