From 9cf2d09c3486302be4af32acd8ee7a8aeb3b3960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 May 2021 12:21:58 +0200 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=94=A5=20Remove=20support=20for=20HTM?= =?UTF-8?q?L=20comments=20to=20avoid=20rate=20limits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit and because that feature was not used much after having support for labels --- app/main.py | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/app/main.py b/app/main.py index 8e14d9e..b4c14b7 100644 --- a/app/main.py +++ b/app/main.py @@ -7,13 +7,11 @@ 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." remove_label_on_comment: bool = True remove_label_on_close: bool = False @@ -87,7 +85,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()) @@ -127,24 +125,6 @@ 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 if __name__ == "__main__": @@ -153,7 +133,6 @@ 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() @@ -165,8 +144,9 @@ def process_issue(*, issue: Issue, settings: Settings, owner: NamedUser) -> None 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) + 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") From d302d7bbdb542c2452fc5899d3b245a1a513b9cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 May 2021 18:43:46 +0200 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=94=8A=20Add=20not=20closing=20loggin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/main.py b/app/main.py index b4c14b7..3c9ccb7 100644 --- a/app/main.py +++ b/app/main.py @@ -93,12 +93,10 @@ def process_issue(*, issue: Issue, settings: Settings) -> 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') @@ -125,6 +123,10 @@ def process_issue(*, issue: Issue, settings: Settings) -> None: 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__": From eb1f95b5bbe06d98dbe4f9f51d4ad56bc1e17fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 May 2021 19:00:11 +0200 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=93=8C=20Pin=20ranges=20of=20PyGitHub?= =?UTF-8?q?=20and=20pydantic=20better?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From f3f82a6b58be153b2f283f7d68fda027555d22a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 May 2021 19:34:20 +0200 Subject: [PATCH 4/7] =?UTF-8?q?=E2=9C=A8=20Enable=20processing=20PRs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 3c9ccb7..1b04e00 100644 --- a/app/main.py +++ b/app/main.py @@ -12,7 +12,7 @@ class KeywordMeta(BaseModel): delay: timedelta = timedelta(days=10) - 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 @@ -141,6 +141,7 @@ def process_issue(*, issue: Issue, settings: Settings) -> None: 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: From b904057ee238005404f101294f7933cae2751fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 May 2021 19:53:35 +0200 Subject: [PATCH 5/7] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20reading?= =?UTF-8?q?=20PR=20event=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/main.py b/app/main.py index 1b04e00..18703af 100644 --- a/app/main.py +++ b/app/main.py @@ -37,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]: @@ -144,10 +145,16 @@ def process_issue(*, issue: Issue, settings: Settings) -> None: 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) + 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 keyword, keyword_meta in settings.input_config.items(): for issue in repo.get_issues(state="open", labels=[keyword]): From a114463a309906273aa61b4b632fd3a16a7777f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 May 2021 20:07:44 +0200 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20supporting?= =?UTF-8?q?=20PRs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 99 +++++++++++++++++++++++++++---------------------------- 1 file changed, 48 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 81b81f9..2ac7c35 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: +And finally, if: -```markdown -Ah, you have to use a JSON string in the config. - - -``` +* 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,6 +207,10 @@ on: issues: types: - labeled + pull_request_target: + types: + - labeled + workflow_dispatch: jobs: issue-manager: @@ -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,6 +273,10 @@ on: issues: types: - labeled + pull_request_target: + types: + - labeled + workflow_dispatch: jobs: issue-manager: @@ -285,30 +289,18 @@ jobs: { "$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 From 25196e4b1295e8ae8356b5353aeb18e9e3eb49be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 May 2021 20:12:07 +0200 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=93=9D=20Update=20README=20with=20new?= =?UTF-8?q?=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2ac7c35..6e0106c 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ 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: > @@ -282,7 +282,7 @@ 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: >