commit bf153fe2dfdee39b21045e8fcf29e60b16fd647c Author: alstr Date: Sat Mar 7 15:06:00 2020 +0000 Initial commit diff --git a/.github/workflows/workflow.yaml b/.github/workflows/workflow.yaml new file mode 100644 index 0000000..b2b6fb7 --- /dev/null +++ b/.github/workflows/workflow.yaml @@ -0,0 +1,17 @@ +name: "Workflow" +on: ["push"] +jobs: + build: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@master" + - name: "TODO to Issue" + uses: "alstr/todo-to-issue-action@master" + with: + REPO: ${{ github.repository }} + HEAD: ${{ github.event.head }} + BEFORE: ${{ github.event.before }} + SHA: ${{ github.sha }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + LABEL: "# TODO" + id: "todo" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4a5e494 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3-slim AS builder +ADD . /app +WORKDIR /app + +RUN pip install --target=/app requests + +FROM gcr.io/distroless/python3-debian10 +COPY --from=builder /app /app +WORKDIR /app +ENV PYTHONPATH /app +CMD ["/app/main.py"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e7f655 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# TODO to Issue Action + +This action will convert your `# TODO` comments to GitHub issues when a new commit is pushed. + +It will also close an issue when a `# TODO` is removed in a pushed commit. + +The `# TODO` comment is commonly used in Python, but this can be customised to whatever you want. + +## Usage + +Create a workflow file in your .github/workflows directory as follows: + +### workflow.yaml + + name: "Workflow" + on: ["push"] + jobs: + build: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@master" + - name: "TODO to Issue" + uses: "alstr/todo-to-issue-action@master" + with: + REPO: ${{ github.repository }} + BEFORE: ${{ github.event.before }} + SHA: ${{ github.sha }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + LABEL: "# TODO" + id: "todo" + +### Inputs + +| Input | Description | +|----------|-------------| +| `REPO` | The path to the repository where the action will be used, e.g. 'alstr/my-repo' (automatically set) | +| `BEFORE` | The SHA of the last pushed commit (automatically set) | +| `SHA` | The SHA of the latest commit (automatically set) | +| `TOKEN` | The GitHub access token to allow us to retrieve, create and update issues (automatically set) | +| `LABEL` | The label that will be used to identify TODO comments (by default this is `# TODO` for Python) | + +## Examples + +### Adding TODOs + + def hello_world(): + # TODO Come up with a more imaginative greeting + print('Hello world!') + +This will create an issue called "Come up with a more imaginative greeting". + +**The action expects a space to follow the `TODO` label.** + +Should the title be longer than 50 characters, it will be truncated for the issue title. + +The full title will be included in the issue body and a `todo` label will be attached to the issue. + +### Removing TODOs + + def hello_world(): + print('Hello world!') + +Removing the `# TODO` comment will close the issue on push. + +### Updating TODOs + + def hello_world(): + # TODO Come up with a more imaginative greeting, like "Greetings world!" + print('Hello world!') + +Should you change the `# TODO` text, this will currently create a new issue, so bear that in mind. + +This may be updated in future. + +## Thanks + +Thanks to Jacob Tomlinson for his handy overview of GitHub Actions: + +https://www.jacobtomlinson.co.uk/posts/2019/creating-github-actions-in-python/ \ No newline at end of file diff --git a/action.yaml b/action.yaml new file mode 100644 index 0000000..3630275 --- /dev/null +++ b/action.yaml @@ -0,0 +1,6 @@ +name: "TODO to Issue" +description: "Converts IDE TODO comments to GitHub issues" +author: "Alastair Mooney" +runs: + using: "docker" + image: "Dockerfile" \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..f3bc141 --- /dev/null +++ b/main.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +"""Convert IDE TODOs to GitHub issues.""" + +import os +import requests +import re +import json +from time import sleep + + +base_url = 'https://api.github.com/repos/' + + +def main(): + repo = os.getenv('INPUT_REPO') + before = os.getenv('INPUT_BEFORE') + sha = os.getenv('INPUT_SHA') + label = os.getenv('INPUT_LABEL') + params = { + 'access_token': os.getenv('INPUT_TOKEN') + } + + # Let's compare the last two pushed commits. + diff_url = f'{base_url}{repo}/compare/{before}...{sha}' + diff_headers = { + 'Accept': 'application/vnd.github.v3.diff' + } + + diff_request = requests.get(url=diff_url, headers=diff_headers, params=params) + if diff_request.status_code == 200: + diff = diff_request.text + + # Check for additions in the diff. + addition_pattern = re.compile(r'(?<=^\+).*', re.MULTILINE) + additions = addition_pattern.findall(diff) + new_issues = [] + + # Filter the additions down to newly added TODOs. + for addition in additions: + todo_pattern = re.compile(r'(?<=' + label + r'\s).*') + todos = todo_pattern.search(addition) + if todos: + new_issues.append(todos.group(0)) + + # Create new issues for any newly added TODOs. + issues_url = f'{base_url}{repo}/issues' + issue_headers = { + 'Content-Type': 'application/json', + } + for issue in new_issues: + title = issue + # Truncate the title if it's longer than 50 chars. + if len(title) > 50: + title = issue[:50] + '...' + new_issue_body = {'title': title, 'body': issue, 'labels': ['todo']} + requests.post(url=issues_url, headers=issue_headers, params=params, data=json.dumps(new_issue_body)) + # Don't add too many issues too quickly. + sleep(1) + + # Check for deletions in the diff. + deletion_pattern = re.compile(r'(?<=^-).*', re.MULTILINE) + deletions = deletion_pattern.findall(diff) + closed_issues = [] + + # Filter the deletions down to removed TODOs. + for deletion in deletions: + todo_pattern = re.compile(r'(?<=' + label + r'\s).*') + todos = todo_pattern.search(deletion) + if todos: + closed_issues.append(todos.group(0)) + + if len(closed_issues) > 0: + # Get the list of current issues. + list_issues_request = requests.get(issues_url, headers=issue_headers, params=params) + if list_issues_request.status_code == 200: + current_issues = list_issues_request.json() + for closed_issue in closed_issues: + title = closed_issue + if len(title) > 50: + title = closed_issue[:50] + '...' + + # Compare the title of each closed issue with each issue in the issues list. + for current_issue in current_issues: + if current_issue['title'] == title: + # The titles match, so we will try and close the issue. + issue_number = current_issue['number'] + + update_issue_url = f'{base_url}{repo}/issues/{issue_number}' + body = {'state': 'closed'} + requests.patch(update_issue_url, headers=issue_headers, params=params, + data=json.dumps(body)) + # Don't update too many issues too quickly. + sleep(1) + + +if __name__ == "__main__": + main()