-
Notifications
You must be signed in to change notification settings - Fork 31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comment on stale issues #109
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'; | ||
} |
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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, but could we add a
And, of course, the Thanks! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The ´--dry-run´ option is added. It will show you 100 issues on Symfony/symfony on the first run. We do not use pagination because it isn't really needed. Here is a link to the search: https://github.com/symfony/symfony/issues?q=is%3Aissue+-linked%3Apr+-label%3A%22Keep+open%22+is%3Aopen+updated%3A%3C2019-12-18 |
||
$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; | ||
} | ||
} |
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; | ||
} | ||
} |
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(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is not using pagination. We will only fetch 100 comments. That means that we will never auto close issues with more that 100 comments.
Im fine with that atm.