# todo-to-issue-action: turning TODO comments into GitHub issues on push

> todo-to-issue-action is a GitHub Action that creates, updates and closes issues from TODO comments committed to a repository, with references that set assignees and labels, options for milestones and projects, and optional URL insertion that writes the issue link back into the source. It fits teams that want a backlog generated from code rather than from memory.

**alstr/todo-to-issue-action** — Action that converts TODO comments to GitHub issues on push.

- Repository: https://github.com/alstr/todo-to-issue-action
- Stars: 800 · Forks: 132
- Language: Python
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/alstr-todo-to-issue-action

## A backlog that comes out of the source tree

todo-to-issue-action is a GitHub Action that creates, updates and closes issues based on committed TODO comments. The trigger is a push. When the action runs, it reads the repository, finds comments that begin with a configured identifier, and turns each one into a GitHub issue.

The problem it solves is narrow and real. TODO comments are cheap to write and expensive to lose. They sit in a diff, they get copied between branches, and they are invisible to anyone who does not open the file. Once the same note exists as an issue, it has an owner, a label, a milestone and a place in a board. The action is the bridge between those two worlds, and its value depends entirely on whether your team already writes TODO comments.

The identifier is configurable, so the action is not limited to the literal word TODO. FIXME and similar markers are supported, which matters for codebases that already have a convention. The README also notes that the action works with almost any programming language, and the supported list runs to more than sixty entries, from C and Python through Haskell, Solidity, and markup languages such as Markdown and YAML.

The features list adds the pieces that turn a plain converter into something a team can run: configurable auto-labelling, assignees, milestones and projects. Each of those can come from the comment itself, which is the mechanism that makes the difference between a converter and a workflow.

The repository ships a diagram image named diagram.png that the README places right after the one-line description, which is a sensible way to explain a system whose behaviour is otherwise defined by its options.

## The reference syntax that assigns work and links issues

A TODO can carry a single reference, written in parentheses right after the identifier, and the reference decides what happens to the resulting issue. The README shows four cases in a single Python example:

```python
def hello_world():
  # TODO(@alstr): Come up with a more imaginative greeting
  #  This will assign the issue to alstr.

  # TODO(!urgent): This is wrong
  #  This will add an 'urgent' label.

  # TODO(#99): We need error handling here
  #  This will add the comment to the existing issue 99.

  # TODO(language): Localise this string
  #  This will prepend the reference to the issue title
```

An at sign assigns the issue to that GitHub user. An exclamation mark adds a label. A hash refers to an existing issue, and the comment is added to that issue rather than creating a new one. A plain word is treated as a reference that is prepended to the issue title, which is how you tag a TODO with a category without turning it into a label.

Only one reference can be provided, and that is the first constraint worth knowing about. If a TODO needs both an owner and a label, the second one has to go in an options line instead. The README points to the options section for exactly that case.

The reference syntax follows the Google Style Guide convention for TODO comments, which is why the documentation links it. That matters more than it might seem: a convention borrowed from a widely read style guide is one your contributors will already recognise, and a convention invented by the action is one they will not.

## Options lines for assignees, labels and milestones

Anything the reference cannot express goes into an options line, and options use a `name: value` syntax. Unless otherwise specified they sit on their own line below the initial TODO declaration and body. The canonical example is the same three-line TODO with an options line appended:

```python
def hello_world():
    # TODO: Come up with a more imaginative greeting
    #  Everyone uses hello world and it's boring.
    #  labels: enhancement, help wanted
    print('Hello world!')
```

The multiline form is what makes this usable. The first line after the TODO marker is the issue title, and every additional indented line becomes part of the issue body. That is how a developer keeps the detail next to the code without it ending up in the title.

Three option keys are documented. Assignees take a comma-separated list of usernames, labels take a comma-separated list of label names, and milestone takes a single milestone name. Two behaviours differ between them and are easy to miss. Labels that do not already exist are created. A milestone that does not exist is also created, but only one milestone can be specified.

Assignees are the option most likely to be misused. A comma-separated list hard-coded in a comment ties a piece of work to a person, and when that person leaves, the comment becomes stale rather than disappearing. Labels are safer, since a label like `enhancement` outlives the people using it. Milestones are the most dangerous of the three in the same way, because a milestone name in a comment pins work to a release that may be renamed.

The TODO options section also notes that the options apply in addition to any reference supplied, so the two mechanisms combine rather than conflict.

## The workflow file and the duplicate issue trap

The setup has two steps, and the first one is not in the repository. In the repo where the action should run, go to Settings, then Actions (General), then Workflow permissions, and enable read and write permissions. Without that, the action has nothing to create issues with.

The second step is a workflow file in `.github/workflows`:

```yml
name: "Run TODO to Issue"
on: [ "push" ]
jobs:
  build:
    runs-on: "ubuntu-latest"
    permissions:
      issues: write
    steps:
      - uses: "actions/checkout@v6"
      - name: "TODO to Issue"
        uses: "alstr/todo-to-issue-action@v5"
```

The permissions block is scoped to the job and grants only `issues: write`, which is the narrow form of the grant rather than a blanket write token. The checkout step is required because the action works on the committed files.

The warning that follows this example is the part to read twice. If you set multiple triggers, that is both `pull_request` and `push`, the result can be duplicate issues. This is the classic continuous integration mistake of a job that runs on the same commit twice, and here the artifact is user visible noise in the issue tracker. Pick one trigger.

Two further details are worth noticing. The action is pinned to a major version tag, `v5`, so a new minor release is picked up without a workflow edit. And the workflow runs on `ubuntu-latest`, a GitHub hosted runner, which means the action executes in GitHub infrastructure with the repository token.

## URL insertion writes the issue link back into the source

The most interesting feature in the project is also the one that is disabled by default. URL insertion takes the URL of a created issue and writes it back into the associated TODO comment. The README gives three reasons: it lets issues and TODOs stay tightly integrated, it enables updating issues by editing TODOs, and it improves the accuracy of the action when closing TODOs.

That last point is the mechanism that makes the feature more than cosmetic. If the TODO text contains the issue URL, the action can tell which issue belongs to which comment, so closing is reliable rather than heuristic.

Enabling it needs more than a flag. The workflow gains extra permissions and two extra steps that commit the rewritten source back to the repository:

```yml
      - name: "TODO to Issue"
        uses: "alstr/todo-to-issue-action@v5"
        with:
          INSERT_ISSUE_URLS: "true"
      - name: Set Git user
        run: |
          git config --global user.name "github-actions[bot]"
          git config --global user.email "github-actions[bot]@users.noreply.github.com"
      - name: Commit and Push Changes
        run: |
          git add -A
          if [[ `git status --porcelain` ]]; then
            git commit -m "Automatically added GitHub issue links to TODOs"
            git push origin main
          else
            echo "No changes to commit"
          fi
```

The consequence is that the action now commits to your default branch on every run. That is a meaningful change to the repository's history, and it needs `contents: write` and `pull-requests: write` alongside `issues: write`. The README pairs the feature with the advice to also set `CLOSE_ISSUES: "true"` so that issues close when the TODO is removed.

The documentation is honest about the risk in one line: this feature is not perfect, and you should be comfortable with that before enabling it. Enabling it on a branch first and reading the resulting diff is the sensible way to find out how your particular code style reacts to the insertion, since the action has to rewrite comment text and the indentation of a multiline TODO is the part most likely to shift.

## Language coverage through syntax.json and the parser modules

How the action finds a TODO in a file is the part of the design that determines whether it works on your codebase. The answer is a data file. New languages can easily be added to the `syntax.json` file used by the action to identify TODO comments, and pull requests adding languages are welcomed.

That is a deliberate architectural choice with two consequences. Adding a language does not require touching the parser, so support spreads through a data change rather than a code change. And the set of languages is a configuration artefact a maintainer can extend without reading the implementation, which is why the language list in the README runs to more than sixty entries and includes niche formats such as Agda, Move, Solidity, Starlark, Visual Basic for Applications and Nix alongside the obvious ones.

The Python implementation is split into small modules with revealing names at the repository root: `main.py` as the entry point, `TodoParser.py` for finding and reading comments, `Issue.py` for the issue model, `LineStatus.py` for tracking the state of each line, and three client modules, `Client.py`, `GitHubClient.py` and `LocalClient.py`. The separation of a GitHub client from a base client, with a local implementation alongside it, suggests the tool is structured so that the issue creating side can be exercised without talking to GitHub.

The action metadata lives in `action.yml`, and the dependency list is short and pinned: `requests==2.32.3` and `ruamel.yaml==0.18.6`. Pinning exactly is the right call for an action that runs in other people's workflows, because a floating dependency in a bot token context is a supply chain risk. The test suite lives in `tests/`, and a `.devcontainer` directory is present for local development.

## Running the action on your own runner

The Dockerfile in the repository root shows how the action is packaged, and it is worth reading because it defines the two ways to run it. The build stage is based on `python:3-slim` and copies the Python files into `/app`, installing the dependencies into a target directory so they land in the final image without a virtual environment. Two runtime stages follow. One is based on `ubuntu` and installs `python3` and `git`, which matters because the runtime needs a git client for the URL insertion commit step. The other is based on `gcr.io/distroless/python3-debian12`, a runtime with no shell and no package manager, which is the smaller and harder to inspect option.

That gives you a choice of deployment model. The documented path is the GitHub Action on a GitHub hosted runner, where the workflow in the repository is all you need. The container image is the alternative, and it is the one that lets you run the same code on a self hosted runner, inside your own network, or as a scheduled job outside the pull request and push flow. The distroless runtime is the better fit for the second case because it has nothing in it to exec.

The trade-off in the distroless image is that without a shell, the git commands the URL insertion workflow relies on cannot be run from inside the container, which is precisely why the ubuntu runtime stage installs `git` explicitly. If you build your own image, pick that stage.

The licence is MIT, with the licence file at the repository root, and the release history shows v5 released on 2024-11-26 with v5.1.14 on 2026-02-03 and v5.1.15 on 2026-07-06, which is the date of the last push. The repository is not archived, and the v5 release notes describe breaking changes in an upgrading section, so teams moving from v4 should read that section before pinning `v5`.

## Conclusion

Adopt todo-to-issue-action when your team already writes TODO comments in code and wants them tracked without a second place to update, especially if you also use the reference syntax to route work to owners and labels. Do not adopt it if the repository has no write permission culture, since the action needs read and write permissions and pushes commits back when URL insertion is on, or if your issue tracker is not GitHub Issues. Verify first that you have exactly one trigger configured, since both pull_request and push produce duplicate issues, then enable INSERT_ISSUE_URLS in a branch and read the diff, because the project itself states that this feature is not perfect. Check the version you pin, since v5 changed the issue format and the release history shows v5.1.15 on 2026-07-06.

## FAQ

### How do I set up the todo-to-issue-action in my repository?

Enable read and write permissions under Settings, Actions (General), then Workflow permissions, and add a workflow file in .github/workflows that checks out the repository and uses alstr/todo-to-issue-action@v5 with the issues: write permission. Configure only one trigger, because both pull_request and push can create duplicate issues.

### How do I assign a GitHub user or add a label with a TODO comment?

Add a single reference in parentheses after the identifier. An at sign such as TODO(@alstr) assigns the issue to that user, an exclamation mark such as TODO(!urgent) adds a label, a hash such as TODO(#99) adds the comment to the existing issue, and a plain word such as TODO(language) is prepended to the issue title.

### What is the INSERT_ISSUE_URLS setting in todo-to-issue-action?

It is disabled by default and, when set to true, inserts the URL of a created issue back into the associated TODO. It requires extra permissions and steps that commit and push the rewritten source back to the repository, and the README states that the feature is not perfect, so enable it on a branch and review the diff first.

### Can todo-to-issue-action parse TODOs in a language it does not list?

Yes. New languages can easily be added to the syntax.json file used by the action to identify TODO comments, and the project welcomes pull requests that add them. The action already covers more than sixty languages, from C and Python to Solidity, Starlark and Visual Basic for Applications.

### What changed in version 5 of todo-to-issue-action?

The project calls v5 its biggest release yet, adding TODO reference handling, issue URL insertion, updating and commenting on existing issues, support for v2 projects, assigning milestones by name, improved issue formatting, and linking issues to pull requests. The README points to an upgrading section for the breaking changes.

## Sources

- [alstr/todo-to-issue-action on GitHub](https://github.com/alstr/todo-to-issue-action)
- [Issues](https://github.com/alstr/todo-to-issue-action/issues)
- [License: MIT](https://github.com/alstr/todo-to-issue-action/blob/master/LICENSE)
- [README](https://github.com/alstr/todo-to-issue-action/blob/master/README.md)
- [Releases](https://github.com/alstr/todo-to-issue-action/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/alstr-todo-to-issue-action
