diff --git a/Dockerfile b/Dockerfile index dc37b72..c2a9724 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM python:3.7 -RUN pip install PyGithub "pydantic==1.5.1" +RUN pip install "PyGithub>=1.55,<2.0" "pydantic>=v1.8.2,<2.0" COPY ./app /app diff --git a/README.md b/README.md index 81b81f9..6e0106c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Issue Manager -Automatically close issues that have a **label**, after a **custom delay**, if no one replies back. +Automatically close issues or Pull Requests that have a **label**, after a **custom delay**, if no one replies back. ## How to use @@ -17,32 +17,35 @@ on: issue_comment: types: - created - - edited issues: types: - labeled + pull_request_target: + types: + - labeled + workflow_dispatch: jobs: issue-manager: runs-on: ubuntu-latest steps: - - uses: tiangolo/issue-manager@0.3.0 + - uses: tiangolo/issue-manager@0.4.0 with: token: ${{ secrets.GITHUB_TOKEN }} config: '{"answered": {}}' ``` -Then, you can answer an issue and add the label from the config, in this case, `answered`. +Then, you can answer an issue or PR and add the label from the config, in this case, `answered`. After 10 days, if no one has added a new comment, the GitHub action will write: ```markdown -Assuming the original issue was solved, it will be automatically closed now. +Assuming the original need was handled, this will be automatically closed now. ``` And then it will close the issue. -But if someone adds a comment _after_ you added the label, it will remove the label. +But if someone adds a comment _after_ you added the label, this GitHub Action will remove the label so that you can come back and check it instead of closing it. ## Config @@ -77,6 +80,10 @@ Imagine this JSON config: "waiting": { "delay": 691200, "message": "Closing after 8 days of waiting for the additional info requested." + }, + "needs-tests": { + "delay": 691200, + "message": "This PR will be closed after waiting 8 days for tests to be added. Please create a new one with tests." } } ``` @@ -113,11 +120,11 @@ And also, if there was a new comment created _after_ the label was added, by def --- -And in the last case, if: +Then, if: * the issue has a label `waiting` * the label was added _after_ the last comment -* the last comment was addded more than `691200` seconds (10 days) ago +* the last comment was addded more than `691200` seconds (8 days) ago ...the GitHub action would close the issue with: @@ -127,41 +134,31 @@ Closing after 10 days of waiting for the additional info requested. And again, by default, removing the label if there was a new comment written after adding the label. -### Delay - -The delay can be configured using [anything supported by Pydantic's `datetime`](https://pydantic-docs.helpmanual.io/usage/types/#datetime-types). - -So, it can be an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) period format (like `P3DT12H30M5S`), or the amount of seconds between the two dates (like `691200`, or 10 days) plus other options. - -### Users and HTML comments - -Before supporting labels, this GitHub action used HTML comments, so, you would write a comment like: +--- -```markdown -Ah, you have to use a JSON string in the config. +And finally, if: - -``` +* a PR has a label `needs-tests` +* the label was added _after_ the last comment +* the last comment was addded more than `691200` seconds (8 days) ago -Then the comment would only show: +...the GitHub action would close the PR with: ```markdown -Ah, you have to use a JSON string in the config. +This PR will be closed after waiting 8 days for tests to be added. Please create a new one with tests. ``` -And the GitHub action would read the label/keyword from that HTML comment. +**Note**: in this last example the process is applied to a PR instead of an issue. The same logic applies to both issues and PRs. If you want a label to only apply to issues, you should use that label only with issues, and the same with PRs. -To support external users adding these comments (even if they can't add labels to your repo), you can add a config `users` with a list of usernames allowed to add these HTML keyword comments. +### Delay -In this case, the GitHub action will only close the issue if: +The delay can be configured using [anything supported by Pydantic's `datetime`](https://pydantic-docs.helpmanual.io/usage/types/#datetime-types). -* the _last_ comment has the keyword/label -* it was written by a user in the `users` list in the `config` (or the owner of the repo) -* the time delay since the last comment is enough +So, it can be an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) period format (like `P3DT12H30M5S`), or the number of seconds between the two dates (like `691200`, or 10 days) plus other options. ### Remove label on comment -You can also pass a config `remove_label_on_comment` per keyword. By default it's `true`. +You can also pass a config `remove_label_on_comment` per keyword. By default, it's `true`. When someone adds a comment _after_ the label was added, then this GitHub action won't close the issue. @@ -177,7 +174,6 @@ By default it is false, and doesn't remove the label from the issue. By default, any config has: -* `users`: No users, only the repository owner (only applies to HTML comments). * `delay`: A delay of 10 days. * `message`: A message of: @@ -211,12 +207,16 @@ on: issues: types: - labeled + pull_request_target: + types: + - labeled + workflow_dispatch: jobs: issue-manager: runs-on: ubuntu-latest steps: - - uses: tiangolo/issue-manager@0.3.0 + - uses: tiangolo/issue-manager@0.4.0 with: token: ${{ secrets.GITHUB_TOKEN }} config: > @@ -238,7 +238,7 @@ jobs: ### Edit your own config -If you have [Visual Studio Code](https://code.visualstudio.com) or other modern editor, you can create your JSON config by creating a JSON file, e.g. `config.json`. +If you have [Visual Studio Code](https://code.visualstudio.com) or another modern editor, you can create your JSON config by creating a JSON file, e.g. `config.json`. Then writing the contents of your config in that file, and then copying the results. @@ -252,13 +252,13 @@ You can start your JSON config file with: } ``` -And then after you write a keyword and start its config, like `"answered": {}`, it will autocomplete the internal config keys, like `delay`, `users`, `message`. And will validate its contents. +And then after you write a keyword and start its config, like `"answered": {}`, it will autocomplete the internal config keys, like `delay`, `message`. And will validate its contents. It's fine to leave the `$schema` in the `config` on the `.yml` file, it will be discarded and won't be used as a label. ### A complete example -**Note**: you probably don't need all the configs, the examples above should suffice for most cases. But if you want to allow other users to use keywords/labels in HTML comments, or want to make the GitHub action _not_ remove the labels if someone adds a new comment, this can help as an example: +**Note**: you probably don't need all the configs, the examples above should suffice for most cases. But if you want to make the GitHub action _not_ remove the labels if someone adds a new comment, this can help as an example: ```yml name: Issue Manager @@ -273,42 +273,34 @@ on: issues: types: - labeled + pull_request_target: + types: + - labeled + workflow_dispatch: jobs: issue-manager: runs-on: ubuntu-latest steps: - - uses: tiangolo/issue-manager@0.3.0 + - uses: tiangolo/issue-manager@0.4.0 with: token: ${{ secrets.GITHUB_TOKEN }} config: > { "$schema": "https://raw.githubusercontent.com/tiangolo/issue-manager/master/schema.json", "answered": { - "users": [ - "tiangolo", - "dmontagu" - ], "delay": "P3DT12H30M5S", "message": "It seems the issue was answered, closing this now.", "remove_label_on_comment": false, "remove_label_on_close": false }, "validated": { - "users": [ - "tiangolo", - "samuelcolvin" - ], "delay": 300, "message": "The issue could not be validated after 5 minutes. Closing now.", "remove_label_on_comment": true, "remove_label_on_close": false }, "waiting": { - "users": [ - "tomchristie", - "dmontagu" - ], "delay": 691200, "message": "Closing after 8 days of waiting for the additional info requested.", "remove_label_on_comment": true, @@ -332,6 +324,10 @@ on: issues: types: - labeled + pull_request_target: + types: + - labeled + workflow_dispatch: ``` * The `cron` option means that the GitHub action will be run every day at 00:00 UTC. @@ -339,14 +335,17 @@ on: * This way, if there's a new comment, it can immediately remove any label that was added before the new comment. * The `issues` option with a type of `label` will run it with each specific issue when you add a label. * This way you can add a label to an issue that was answered long ago, and if the configured delay since the last comment is enough the GitHub action will close the issue right away. +* The `pull_request_target` option with a type of `label` will run it with each specific Pull Request made to your repo when you add a label. + * This way you can add a label to a PR that was answered long ago, or that was waiting for more comments from the author, etc. And if the configured delay since the last comment is enough the GitHub action will close the issue right away. +* The `workflow_dispatch` option allows you to run the action manually from the GitHub Actions tab for your repo. ## Motivation ### Closing early -When I answer an issue, I like to give the original user some time to respond, and give them the chance to close the issue before doing it myself. +When I answer an issue, I like to give the original user some time to respond and give them the chance to close the issue before doing it myself. -Or some times, I have to request additional info. +Or sometimes, I have to request additional info. Sometimes, my answer didn't respond the real question/problem, and if I closed the issue immediately, it would end up feeling "impolite" to the user. @@ -364,7 +363,7 @@ But that requires me going through all the open issues again, one by one, check One option would be to use a tool that closes stale issues, like [probot/stale](https://github.com/probot/stale), or the [Close Stale Issues Action](https://github.com/marketplace/actions/close-stale-issues). -But if the user came back explaining that my answer didn't respond to his/her problem, or giving the extra info requested, but I couldn't respond on time, the issue would still go "stale" and be closed. +But if the user came back explaining that my answer didn't respond to his/her problem or giving the extra info requested, but I couldn't respond on time, the issue would still go "stale" and be closed. ## What Issue Manager does @@ -379,8 +378,6 @@ Then, this action, by running every night (or however you configure it) will, fo * Then, if all that matches, it will add a comment with a message (configurable). * And then it will close the issue. -Also, all that with the optional alternative using HTML comments. - It will also run after each comment or label added, with the specific issue that has the new comment or label (if you used the example configurations from above). ## Release Notes diff --git a/app/main.py b/app/main.py index 8e14d9e..18703af 100644 --- a/app/main.py +++ b/app/main.py @@ -7,14 +7,12 @@ from github.Issue import Issue from github.IssueComment import IssueComment from github.IssueEvent import IssueEvent -from github.NamedUser import NamedUser from pydantic import BaseModel, BaseSettings, SecretStr, validator class KeywordMeta(BaseModel): delay: timedelta = timedelta(days=10) - users: List[str] = [] - message: str = "Assuming the original issue was solved, it will be automatically closed now." + message: str = "Assuming the original need was handled, this will be automatically closed now." remove_label_on_comment: bool = True remove_label_on_close: bool = False @@ -39,6 +37,7 @@ class PartialGitHubEventIssue(BaseModel): class PartialGitHubEvent(BaseModel): issue: Optional[PartialGitHubEventIssue] = None + pull_request: Optional[PartialGitHubEventIssue] = None def get_last_comment(issue: Issue) -> Optional[IssueComment]: @@ -87,7 +86,7 @@ def close_issue( issue.remove_from_labels(keyword) -def process_issue(*, issue: Issue, settings: Settings, owner: NamedUser) -> None: +def process_issue(*, issue: Issue, settings: Settings) -> None: logging.info(f"Processing issue: #{issue.number}") label_strs = set([label.name for label in issue.get_labels()]) events = list(issue.get_events()) @@ -95,12 +94,10 @@ def process_issue(*, issue: Issue, settings: Settings, owner: NamedUser) -> None last_comment = get_last_comment(issue) for keyword, keyword_meta in settings.input_config.items(): # Check closable delay, if enough time passed and the issue could be closed - closable_delay = False - if ( + closable_delay = ( last_comment is None or (datetime.utcnow() - keyword_meta.delay) > last_comment.created_at - ): - closable_delay = True + ) # Check label, optionally removing it if there's a comment after adding it if keyword in label_strs: logging.info(f'Keyword: "{keyword}" in issue labels') @@ -127,24 +124,10 @@ def process_issue(*, issue: Issue, settings: Settings, owner: NamedUser) -> None label_strs=label_strs, ) break - # Check HTML comments by allowed users - if ( - last_comment - and f"" in last_comment.body - and closable_delay - and last_comment.user.login in keyword_meta.users + [owner.login] - ): - logging.info( - f'Last comment by user: "{last_comment.user.login}" had HTML keyword ' - f'comment: "{keyword}" and there\'s a closable delay.' - ) - close_issue( - issue=issue, - keyword_meta=keyword_meta, - keyword=keyword, - label_strs=label_strs, - ) - break + else: + logging.info( + f"Not clossing issue: #{issue.number} as the delay hasn't been reached: {keyword_meta.delay}" + ) if __name__ == "__main__": @@ -153,20 +136,27 @@ def process_issue(*, issue: Issue, settings: Settings, owner: NamedUser) -> None logging.info(f"Using config: {settings.json()}") g = Github(settings.input_token.get_secret_value()) repo = g.get_repo(settings.github_repository) - owner: NamedUser = repo.owner github_event: Optional[PartialGitHubEvent] = None if settings.github_event_path.is_file(): contents = settings.github_event_path.read_text() github_event = PartialGitHubEvent.parse_raw(contents) if ( settings.github_event_name == "issues" + or settings.github_event_name == "pull_request_target" or settings.github_event_name == "issue_comment" ): - if github_event and github_event.issue: - issue = repo.get_issue(github_event.issue.number) - if issue.state == "open": - process_issue(issue=issue, settings=settings, owner=owner) + if github_event: + issue_number: Optional[int] = None + if github_event.issue: + issue_number = github_event.issue.number + elif github_event.pull_request: + issue_number = github_event.pull_request.number + if issue_number is not None: + issue = repo.get_issue(issue_number) + if issue.state == "open": + process_issue(issue=issue, settings=settings) else: - for issue in repo.get_issues(state="open"): - process_issue(issue=issue, settings=settings, owner=owner) + for keyword, keyword_meta in settings.input_config.items(): + for issue in repo.get_issues(state="open", labels=[keyword]): + process_issue(issue=issue, settings=settings) logging.info("Finished")