-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature #109 Comment on stale issues (Nyholm)
This PR was squashed before being merged into the master branch. Discussion ---------- Comment on stale issues This PR adds a `PingStaleIssuesCommand`. It will look for old inactive issues and start a process with them. 1. Bot will make a comment to encourage activity and add label "Staled". 2. Bot will make a comment to inform the issue will be closed 3. Bot will close the issue. The process can be interrupted with anyone making a comment on the issue or the "Keep open" label is added. The exact times between the steps described above is defined as constants in `PingStaleIssuesCommand`. Commits ------- 9de3045 Comment on stale issues
- Loading branch information
Showing
16 changed files
with
685 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace App\Api\Issue; | ||
|
||
class IssueType | ||
{ | ||
public const BUG = 'Bug'; | ||
public const FEATURE = 'Feature'; | ||
public const UNKNOWN = 'Unknown'; | ||
public const RFC = 'RFC'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
<?php | ||
|
||
namespace App\Command; | ||
|
||
use App\Api\Issue\IssueApi; | ||
use App\Api\Issue\IssueType; | ||
use App\Api\Label\LabelApi; | ||
use App\Entity\Task; | ||
use App\Service\RepositoryProvider; | ||
use App\Service\StaleIssueCommentGenerator; | ||
use App\Service\TaskScheduler; | ||
use Symfony\Component\Console\Command\Command; | ||
use Symfony\Component\Console\Input\InputArgument; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
use Symfony\Component\Console\Input\InputOption; | ||
use Symfony\Component\Console\Output\OutputInterface; | ||
|
||
/** | ||
* Close issues not been updated in a long while. | ||
* | ||
* @author Tobias Nyholm <[email protected]> | ||
*/ | ||
class PingStaleIssuesCommand extends Command | ||
{ | ||
public const STALE_IF_NOT_UPDATED_SINCE = '-12months'; | ||
public const MESSAGE_TWO_AFTER = '+2weeks'; | ||
public const MESSAGE_THREE_AND_CLOSE_AFTER = '+2weeks'; | ||
|
||
protected static $defaultName = 'app:issue:ping-stale'; | ||
|
||
private $repositoryProvider; | ||
private $issueApi; | ||
private $scheduler; | ||
private $commentGenerator; | ||
private $labelApi; | ||
|
||
public function __construct(RepositoryProvider $repositoryProvider, IssueApi $issueApi, TaskScheduler $scheduler, StaleIssueCommentGenerator $commentGenerator, LabelApi $labelApi) | ||
{ | ||
parent::__construct(); | ||
$this->repositoryProvider = $repositoryProvider; | ||
$this->issueApi = $issueApi; | ||
$this->scheduler = $scheduler; | ||
$this->commentGenerator = $commentGenerator; | ||
$this->labelApi = $labelApi; | ||
} | ||
|
||
protected function configure() | ||
{ | ||
$this->addArgument('repository', InputArgument::REQUIRED, 'The full name to the repository, eg symfony/symfony-docs'); | ||
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Do a test search without making any comments or changes'); | ||
} | ||
|
||
protected function execute(InputInterface $input, OutputInterface $output) | ||
{ | ||
/** @var string $repositoryName */ | ||
$repositoryName = $input->getArgument('repository'); | ||
$repository = $this->repositoryProvider->getRepository($repositoryName); | ||
if (null === $repository) { | ||
$output->writeln('Repository not configured'); | ||
|
||
return 1; | ||
} | ||
|
||
$notUpdatedAfter = new \DateTimeImmutable(self::STALE_IF_NOT_UPDATED_SINCE); | ||
$issues = $this->issueApi->findStaleIssues($repository, $notUpdatedAfter); | ||
|
||
if ($input->getOption('dry-run')) { | ||
foreach ($issues as $issue) { | ||
$output->writeln(sprintf('Marking issue #%s as "Staled". Link https://github.com/%s/issues/%s', $issue['number'], $repository->getFullName(), $issue['number'])); | ||
} | ||
|
||
return 0; | ||
} | ||
|
||
foreach ($issues as $issue) { | ||
$comment = $this->commentGenerator->getComment($this->extractType($issue)); | ||
$this->issueApi->commentOnIssue($repository, $issue['number'], $comment); | ||
$this->labelApi->addIssueLabel($issue['number'], 'Staled', $repository); | ||
|
||
// add a scheduled task to process this issue again after 2 weeks | ||
$this->scheduler->runLater($repository, $issue['number'], Task::ACTION_INFORM_CLOSE_STALE, new \DateTimeImmutable(self::MESSAGE_TWO_AFTER)); | ||
} | ||
|
||
return 0; | ||
} | ||
|
||
/** | ||
* Extract type from issue array. Make sure we priorities labels if there are | ||
* more than one type defined. | ||
*/ | ||
private function extractType(array $issue) | ||
{ | ||
$types = [ | ||
IssueType::FEATURE => false, | ||
IssueType::BUG => false, | ||
IssueType::RFC => false, | ||
]; | ||
|
||
foreach ($issue['labels'] as $label) { | ||
if (isset($types[$label['name']])) { | ||
$types[$label['name']] = true; | ||
} | ||
} | ||
|
||
foreach ($types as $type => $exists) { | ||
if ($exists) { | ||
return $type; | ||
} | ||
} | ||
|
||
return IssueType::UNKNOWN; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace App\Service; | ||
|
||
use App\Api\Issue\IssueType; | ||
|
||
/** | ||
* @author Tobias Nyholm <[email protected]> | ||
*/ | ||
class StaleIssueCommentGenerator | ||
{ | ||
/** | ||
* Get a comment to say: "I will close this soon". | ||
*/ | ||
public function getInformAboutClosingComment(): string | ||
{ | ||
$messages = [ | ||
'Hello? This issue is about to be closed if nobody replies.', | ||
'Friendly ping? Should this still be open? I will close if I don\'t hear anything.', | ||
'Could I get a reply or should I close this?', | ||
'Just a quick reminder to make a comment on this. If I don\'t hear anything I\'ll close this.', | ||
'Friendly reminder that this issue exists. If I don\'t hear anything I\'ll close this.', | ||
'Could I get an answer? If I do not hear anything I will assume this issue is resolved or abandoned. Please get back to me <3', | ||
]; | ||
|
||
return $messages[array_rand($messages)]; | ||
} | ||
|
||
/** | ||
* Get a comment to say: "I'm closing this now". | ||
*/ | ||
public function getClosingComment(): string | ||
{ | ||
return <<<TXT | ||
Hey, | ||
I didn't hear anything so I'm going to close it. Feel free to comment if this is still relevant, I can always reopen! | ||
TXT; | ||
} | ||
|
||
/** | ||
* Get a comment that encourage users to reply or close the issue themselves. | ||
* | ||
* @param string $type Valid types are IssueType::* | ||
*/ | ||
public function getComment(string $type): string | ||
{ | ||
switch ($type) { | ||
case IssueType::BUG: | ||
return $this->bug(); | ||
case IssueType::FEATURE: | ||
case IssueType::RFC: | ||
return $this->feature(); | ||
default: | ||
return $this->unknown(); | ||
} | ||
} | ||
|
||
private function bug(): string | ||
{ | ||
return <<<TXT | ||
Hey, thanks for your report! | ||
There has not been a lot of activity here for a while. Is this bug still relevant? Have you managed to find a workaround? | ||
TXT; | ||
} | ||
|
||
private function feature(): string | ||
{ | ||
return <<<TXT | ||
Thank you for this suggestion. | ||
There has not been a lot of activity here for a while. Would you still like to see this feature? | ||
TXT; | ||
} | ||
|
||
private function unknown(): string | ||
{ | ||
return <<<TXT | ||
Thank you for this issue. | ||
There has not been a lot of activity here for a while. Has this been resolved? | ||
TXT; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace App\Service\TaskHandler; | ||
|
||
use App\Api\Issue\IssueApi; | ||
use App\Api\Label\LabelApi; | ||
use App\Entity\Task; | ||
use App\Service\RepositoryProvider; | ||
use App\Service\StaleIssueCommentGenerator; | ||
|
||
/** | ||
* @author Tobias Nyholm <[email protected]> | ||
*/ | ||
class CloseStaleIssuesHandler implements TaskHandlerInterface | ||
{ | ||
private $issueApi; | ||
private $repositoryProvider; | ||
private $labelApi; | ||
private $commentGenerator; | ||
|
||
public function __construct(LabelApi $labelApi, IssueApi $issueApi, RepositoryProvider $repositoryProvider, StaleIssueCommentGenerator $commentGenerator) | ||
{ | ||
$this->issueApi = $issueApi; | ||
$this->repositoryProvider = $repositoryProvider; | ||
$this->labelApi = $labelApi; | ||
$this->commentGenerator = $commentGenerator; | ||
} | ||
|
||
/** | ||
* Close the issue if the last comment was made by the bot and if "Keep open" label does not exist. | ||
*/ | ||
public function handle(Task $task): void | ||
{ | ||
if (null === $repository = $this->repositoryProvider->getRepository($task->getRepositoryFullName())) { | ||
return; | ||
} | ||
$labels = $this->labelApi->getIssueLabels($task->getNumber(), $repository); | ||
if (in_array('Keep open', $labels)) { | ||
$this->labelApi->removeIssueLabel($task->getNumber(), 'Staled', $repository); | ||
|
||
return; | ||
} | ||
|
||
if ($this->issueApi->lastCommentWasMadeByBot($repository, $task->getNumber())) { | ||
$this->issueApi->commentOnIssue($repository, $task->getNumber(), $this->commentGenerator->getClosingComment()); | ||
$this->issueApi->close($repository, $task->getNumber()); | ||
} else { | ||
$this->labelApi->removeIssueLabel($task->getNumber(), 'Staled', $repository); | ||
} | ||
} | ||
|
||
public function supports(Task $task): bool | ||
{ | ||
return Task::ACTION_CLOSE_STALE === $task->getAction(); | ||
} | ||
} |
Oops, something went wrong.