Also check discussion threads for freshness when marking projects as stalled

This commit is contained in:
Alex Cabal 2024-12-15 21:56:16 -06:00
parent cc9cc5f3f8
commit 2449de6f6c
5 changed files with 169 additions and 69 deletions

View file

@ -21,7 +21,7 @@ parameters:
- %rootDir%/../../../scripts/process-pending-payments - %rootDir%/../../../scripts/process-pending-payments
- %rootDir%/../../../scripts/update-ebook-database - %rootDir%/../../../scripts/update-ebook-database
- %rootDir%/../../../scripts/update-patrons-circle - %rootDir%/../../../scripts/update-patrons-circle
- %rootDir%/../../../scripts/update-project-commits - %rootDir%/../../../scripts/update-project-statuses
- %rootDir%/../../../templates - %rootDir%/../../../templates
dynamicConstantNames: dynamicConstantNames:
- SITE_STATUS - SITE_STATUS

View file

@ -13,5 +13,6 @@ CREATE TABLE IF NOT EXISTS `Projects` (
`ManagerUserId` int(11) NOT NULL, `ManagerUserId` int(11) NOT NULL,
`ReviewerUserId` int(11) NOT NULL, `ReviewerUserId` int(11) NOT NULL,
`LastCommitTimestamp` DATETIME NULL DEFAULT NULL, `LastCommitTimestamp` DATETIME NULL DEFAULT NULL,
`LastDiscussionTimestamp` DATETIME NULL DEFAULT NULL,
PRIMARY KEY (`ProjectId`) PRIMARY KEY (`ProjectId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

View file

@ -5,6 +5,7 @@ use function Safe\curl_init;
use function Safe\curl_setopt; use function Safe\curl_setopt;
use function Safe\json_decode; use function Safe\json_decode;
use function Safe\preg_match; use function Safe\preg_match;
use function Safe\preg_match_all;
use function Safe\preg_replace; use function Safe\preg_replace;
use Safe\DateTimeImmutable; use Safe\DateTimeImmutable;
@ -33,6 +34,7 @@ class Project{
public int $ManagerUserId; public int $ManagerUserId;
public int $ReviewerUserId; public int $ReviewerUserId;
public ?DateTimeImmutable $LastCommitTimestamp = null; public ?DateTimeImmutable $LastCommitTimestamp = null;
public ?DateTimeImmutable $LastDiscussionTimestamp = null;
protected Ebook $_Ebook; protected Ebook $_Ebook;
protected User $_ManagerUser; protected User $_ManagerUser;
@ -145,14 +147,21 @@ class Project{
$this->Validate(); $this->Validate();
try{ try{
$this->FetchLatestCommit(); $this->FetchLastDiscussionTimestamp();
}
catch(Exceptions\AppException){
// Pass; it's OK if this fails during creation.
}
try{
$this->FetchLatestCommitTimestamp();
} }
catch(Exceptions\AppException){ catch(Exceptions\AppException){
// Pass; it's OK if this fails during creation. // Pass; it's OK if this fails during creation.
} }
// Don't let the started date be later than the first commit date. This can happen if the producer starts to commit before their project is approved on the mailing list. // Don't let the started date be later than the first commit date. This can happen if the producer starts to commit before their project is approved on the mailing list.
if($this->LastCommitTimestamp !== null && $this->LastCommitTimestamp > $this->Started){ if($this->LastCommitTimestamp !== null && $this->Started > $this->LastCommitTimestamp){
$this->Started = $this->LastCommitTimestamp; $this->Started = $this->LastCommitTimestamp;
} }
@ -181,7 +190,8 @@ class Project{
Ended, Ended,
ManagerUserId, ManagerUserId,
ReviewerUserId, ReviewerUserId,
LastCommitTimestamp LastCommitTimestamp,
LastDiscussionTimestamp
) )
values values
( (
@ -197,9 +207,10 @@ class Project{
?, ?,
?, ?,
?, ?,
?,
? ?
) )
', [$this->EbookId, $this->Status, $this->ProducerName, $this->ProducerEmail, $this->DiscussionUrl, $this->VcsUrl, NOW, NOW, $this->Started, $this->Ended, $this->ManagerUserId, $this->ReviewerUserId, $this->LastCommitTimestamp]); ', [$this->EbookId, $this->Status, $this->ProducerName, $this->ProducerEmail, $this->DiscussionUrl, $this->VcsUrl, NOW, NOW, $this->Started, $this->Ended, $this->ManagerUserId, $this->ReviewerUserId, $this->LastCommitTimestamp, $this->LastDiscussionTimestamp]);
$this->ProjectId = Db::GetLastInsertedId(); $this->ProjectId = Db::GetLastInsertedId();
} }
@ -223,10 +234,11 @@ class Project{
Ended = ?, Ended = ?,
ManagerUserId = ?, ManagerUserId = ?,
ReviewerUserId = ?, ReviewerUserId = ?,
LastCommitTimestamp = ? LastCommitTimestamp = ?,
LastDiscussionTimestamp = ?
where where
ProjectId = ? ProjectId = ?
', [$this->Status, $this->ProducerName, $this->ProducerEmail, $this->DiscussionUrl, $this->VcsUrl, $this->Started, $this->Ended, $this->ManagerUserId, $this->ReviewerUserId, $this->LastCommitTimestamp, $this->ProjectId]); ', [$this->Status, $this->ProducerName, $this->ProducerEmail, $this->DiscussionUrl, $this->VcsUrl, $this->Started, $this->Ended, $this->ManagerUserId, $this->ReviewerUserId, $this->LastCommitTimestamp, $this->LastDiscussionTimestamp, $this->ProjectId]);
if($this->Status == Enums\ProjectStatusType::Abandoned){ if($this->Status == Enums\ProjectStatusType::Abandoned){
Db::Query(' Db::Query('
@ -253,9 +265,9 @@ class Project{
} }
/** /**
* @throws Exceptions\AppException If the operation faile.d * @throws Exceptions\AppException If the operation failed.
*/ */
public function FetchLatestCommit(?string $apiKey = null): void{ public function FetchLatestCommitTimestamp(?string $apiKey = null): void{
$headers = [ $headers = [
'Accept: application/vnd.github+json', 'Accept: application/vnd.github+json',
'X-GitHub-Api-Version: 2022-11-28', 'X-GitHub-Api-Version: 2022-11-28',
@ -292,11 +304,11 @@ class Project{
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if(!is_string($response)){ if(!is_string($response)){
throw new Exceptions\AppException('Response from GitHub was not a string: ' . $response); throw new Exceptions\AppException('Response from <' . $url . '> was not a string: ' . $response);
} }
if($httpCode != Enums\HttpCode::Ok->value){ if($httpCode != Enums\HttpCode::Ok->value){
throw new Exception('HTTP code from GitHub was: ' . $httpCode); throw new Exception('HTTP code ' . $httpCode . ' received for URL <' . $url . '>.');
} }
/** @var array<stdClass> $commits */ /** @var array<stdClass> $commits */
@ -307,7 +319,52 @@ class Project{
} }
} }
catch(Exception $ex){ catch(Exception $ex){
throw new Exceptions\AppException('Error in update-project-commits for URL <' . $url . '>: ' . $ex->getMessage(), 0, $ex); throw new Exceptions\AppException('Error when fetching commits for URL <' . $url . '>: ' . $ex->getMessage(), 0, $ex);
}
}
/**
* @throws Exceptions\AppException If the operation faile.d
*/
public function FetchLastDiscussionTimestamp(): void{
if($this->DiscussionUrl === null){
return;
}
$curl = curl_init($this->DiscussionUrl);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
try{
$response = curl_exec($curl);
/** @var int $httpCode */
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if(!is_string($response)){
throw new Exceptions\AppException('Response from <' . $this->DiscussionUrl . '> was not a string: ' . $response);
}
if($httpCode != Enums\HttpCode::Ok->value){
throw new Exception('HTTP code ' . $httpCode . ' received for URL <' . $this->DiscussionUrl . '>.');
}
$matchCount = preg_match_all('/<span class="[^"]+?">([a-z]{3} [\d]{1,2}, [\d]{4}, [\d]{2}:[\d]{2}:[\d]{2}(?:AM|PM))<\/span>/iu', $response, $matches);
if($matchCount > 0){
// Unsure of the time zone, so just assume UTC.
try{
$this->LastDiscussionTimestamp = new DateTimeImmutable(str_replace('', ' ', $matches[1][sizeof($matches[1]) - 1]));
}
catch(\Exception $ex){
// Failed to parse date, pass.
$this->LastDiscussionTimestamp = null;
}
}
else{
$this->LastDiscussionTimestamp = null;
}
}
catch(Exception $ex){
throw new Exceptions\AppException('Error when fetching discussion for URL <' . $this->DiscussionUrl . '>: ' . $ex->getMessage(), 0, $ex);
} }
} }

View file

@ -1,57 +0,0 @@
#!/usr/bin/php
<?
require_once('/standardebooks.org/web/lib/Core.php');
use function Safe\file_get_contents;
/**
* Iterate over all `Project`s that are in progress or stalled and get their latest GitHub commit. If the commit is more than 30 days old, mark the `Project` as stalled.
*/
use Safe\DateTimeImmutable;
$projects = array_merge(
Project::GetAllByStatus(Enums\ProjectStatusType::InProgress),
Project::GetAllByStatus(Enums\ProjectStatusType::Stalled)
);
$apiKey = trim(file_get_contents('/standardebooks.org/config/secrets/se-vcs-bot@api.github.com'));
$oldestAllowedCommitTimestamp = new DateTimeImmutable('30 days ago');
foreach($projects as $project){
try{
$project->FetchLatestCommit($apiKey);
if(
$project->Status == Enums\ProjectStatusType::InProgress
&&
$project->LastCommitTimestamp !== null
&&
$project->LastCommitTimestamp < $oldestAllowedCommitTimestamp
){
// An active `Project` has stalled.
$project->Status = Enums\ProjectStatusType::Stalled;
}
elseif(
$project->Status == Enums\ProjectStatusType::Stalled
&&
$project->LastCommitTimestamp !== null
&&
$project->LastCommitTimestamp >= $oldestAllowedCommitTimestamp
){
// Revive previously-stalled `Project`s.
$project->Status = Enums\ProjectStatusType::InProgress;
}
$project->Save();
}
catch(Exceptions\AppException $ex){
Log::WriteErrorLogEntry($ex->getMessage());
}
sleep(1);
}

99
scripts/update-project-statuses Executable file
View file

@ -0,0 +1,99 @@
#!/usr/bin/php
<?
require_once('/standardebooks.org/web/lib/Core.php');
use function Safe\file_get_contents;
/**
* Iterate over all `Project`s that are in progress or stalled and get their latest GitHub commit. If the commit is more than 30 days old, mark the `Project` as stalled.
*/
use Safe\DateTimeImmutable;
$projects = array_merge(
Project::GetAllByStatus(Enums\ProjectStatusType::InProgress),
Project::GetAllByStatus(Enums\ProjectStatusType::Stalled)
);
$apiKey = trim(file_get_contents('/standardebooks.org/config/secrets/se-vcs-bot@api.github.com'));
$oldestAllowedTimestamp = new DateTimeImmutable('30 days ago');
foreach($projects as $project){
try{
$project->FetchLastDiscussionTimestamp();
}
catch(Exceptions\AppException $ex){
Log::WriteErrorLogEntry($ex->getMessage());
}
try{
$project->FetchLatestCommitTimestamp($apiKey);
}
catch(Exceptions\AppException $ex){
Log::WriteErrorLogEntry($ex->getMessage());
}
if(
$project->Status == Enums\ProjectStatusType::InProgress
&&
(
(
$project->LastCommitTimestamp !== null
&&
$project->LastDiscussionTimestamp === null
&&
$project->LastCommitTimestamp < $oldestAllowedTimestamp
)
||
(
$project->LastCommitTimestamp !== null
&&
$project->LastDiscussionTimestamp !== null
&&
$project->LastCommitTimestamp < $oldestAllowedTimestamp
&&
$project->LastDiscussionTimestamp < $oldestAllowedTimestamp
)
)
){
// An active `Project` has stalled.
$project->Status = Enums\ProjectStatusType::Stalled;
}
elseif(
$project->Status == Enums\ProjectStatusType::Stalled
&&
(
(
$project->LastCommitTimestamp !== null
&&
$project->LastDiscussionTimestamp === null
&&
$project->LastCommitTimestamp >= $oldestAllowedTimestamp
)
||
(
$project->LastCommitTimestamp !== null
&&
$project->LastDiscussionTimestamp !== null
&&
(
$project->LastCommitTimestamp >= $oldestAllowedTimestamp
||
$project->LastDiscussionTimestamp >= $oldestAllowedTimestamp
)
)
)
){
// Revive previously-stalled `Project`s.
$project->Status = Enums\ProjectStatusType::InProgress;
}
$project->Save();
sleep(1);
}