GitRoot

Craft your forge, Build your project, Grow your community freely

Issues, Tickets, Discussions… Call them what you want!

Many of you, when checking out https://gitroot.dev, have told me that the 🐞 Issues button shouldn’t be labeled “Issues”, but rather “Tickets” or “Discussions”.

First of all, thanks for the feedback! I really appreciate it, that’s how I can build a better forge. Of course, I will change the label, but developing a forge means having a lot of different areas to work on: issues/boards, git, code, CI/CD, and with GitRoot, even more, like plugins and WASM…

So, why don’t I just change this label right away? In reality, this label isn’t hardcoded inside GitRoot. It’s only there because I configured plugins to display it. I want to take this opportunity to explain how GitRoot works, because the platform is developed around this exact concept: how to customize a forge at the project level.

The Original Idea

Imagine you are developing a forge, and to avoid vendor lock-in, you decide not to put anything outside of Git. You choose not to use any database at all. In this context, how do you design an “issue” system?

In traditional forges, an “issue” consists of a title and a description created by a user. We can represent it like this:

AuthorStateIssueint idstring titlestring descriptionStatus<<enumeration>>OpenTODODOINGCLOSEUserint idstring email

Easy, right? Well, how do you represent that in GitRoot using only files? This question was running through my mind for months before I finally found a solution.

The Basic Solution

Since I only have files at my disposal, what actually constitutes an “issue”? I need to add some metadata to a file so the system understands that this file is an issue and not, say, a main.go. I’ll probably want to group all these “issues” into a specific directory, because while GitRoot technically lets you do whatever you want (even dropping an issue right in the middle of your source code), I personally prefer to keep my files organized!

A simple text format that handles metadata perfectly is Markdown. I use Markdown here, but any other markup language could work. Let’s create a file named issues/1.md:

1---
2id: 1
3user: email@email.com
4status: OPEN
5---
6
7# My title
8
9My description where I can detail what my problem is.

Yes, it really is that simple. In GitRoot, to create an issue, you just create a file, add some metadata, commit, and push. Boom, you have an issue.

Now, you might be thinking: “Well, I lose a lot of control with that, the status could be anything!” or “How are other users supposed to know what to put where?”. You’re right, and that is exactly where plugins enter the game.

A Better Solution

If I simplify this as much as possible, what does an external contributor actually need to provide to create an issue? Just a title and a description. So, let’s create a file named issues/my_issue.md:

1# My title
2
3My description where I can detail what my problem is.

Then, we run git commit && git push. Since I have configured the ladybug plugin like this:

 1- name: ladybug
 2  ...
 3  - path: issues/**/*.md
 4    branch:
 5    - "*"
 6    when:
 7    - add
 8    - mod
 9    func: []
10    write:
11      git:
12      - path: issues/**/*.md
13        can:
14        - mod
15      web: []
16      exec: []
17      callfunc: []
18    configuration:
19      metadata:
20      - default: autogenerated
21        mandatory: true
22        name: id
23        type: crc16
24      - default: OPEN
25        mandatory: true
26        name: status
27        type: string
28      - default: email@email.com
29        mandatory: false
30        name: user
31        type: string

You can read this configuration as: “For every Markdown file in the issues directory across all branches, if the file doesn’t have these metadata fields, automatically add these default values.”

Now, if you are a Git purist, a server creating commits right on top of yours might sound a bit radical, or even terrifying. But look closely at the write block in the YAML configuration above:

1write:
2  git:
3    - path: issues/**/*.md
4      can:
5        - mod

GitRoot doesn’t just let plugins run wild, the project configuration must explicitly grant the plugin permission to write or modify files on specific paths. If you don’t declare these rights, GitRoot will block the plugin from making any changes.

When granted, GitRoot intercepts your push, lets the plugin inject the missing metadata, and safely generates a follow-up commit. And don’t worry about your history: if you decide to git rebase -i later to squash or fixup your commits, plugins won’t generate duplicate commits if no metadata has changed.

What about security or external users trying to forge metadata? Since contributors cannot push directly to the default branch (like main), they have to go through a branch and need to be merged. This means every single change to an issue’s status or metadata is subjected to a code review.

If an external user tries to close someone else’s issue or manipulate IDs, it will stand out like a sore thumb in the git diff. Maintainers retain absolute control and can simply reject the rogue contribution. Security doesn’t rely on complex database permission tables, but on the battle-tested git review workflow you already know and trust.

So, once you push, the server-side plugin does its job, and the next time you run git pull, your local file will automatically look like this:

1---
2id: 1
3user: email@email.com
4status: OPEN
5---
6
7# My title
8
9My description where I can detail what my problem is.

Bingo! You get the exact same file, with the exact same metadata you wanted. Of course, a lot more needs to be added, like defining a type: user to assign an issue, or perhaps a type: status to define an enum of allowed values. But I think you see where I’m going with this feature!

(Note: If you’re wondering about the barrier to entry for non-technical contributors, my long-term vision is to allow plugins to intercept standard HTTP requests. Tomorrow, a classic web form will be able to POST to GitRoot and automatically commit a .md file under the hood. But for now, we are proudly dev-centric!)

So, What About the Label?

Throughout all of this, you’ll notice that the term “issue” only appears within the plugins themselves. Nowhere have we talked about that 🐞 Issues button yet. We actually need to introduce another plugin before we can tackle that topic.

First, we need a file that lists all of these “issues”. That’s exactly why the silo plugin exists. It inspects all files and groups them whenever it finds a matching pattern.

To list all open “issues”, we configure it like this:

 1- name: silo
 2  ...
 3  run:
 4  - path: "**/*"
 5    branch:
 6    - main
 7    when:
 8    - add
 9    - mod
10    - del
11    func: []
12    write:
13      git:
14      - path: boards/*.md
15        can:
16        - add
17        - mod
18        - del
19        - append
20      web: []
21      exec: []
22      callfunc: []
23    configuration:
24      boards:
25        description: All open issues
26        for: issues/*.md
27        format: table
28        paginator: 50
29        selects:
30        - "status: (.*)"
31        - "user: (.*)"
32        sort: select[0]
33        sortOrder: desc
34        tableHeader: "| | status | user |"
35        title: All issues
36        to: boards/issues.md
37        where: "status: OPEN"

You can read this configuration as: “For every file in the main branch, create a board called ‘All issues’ containing links to all files matching status: OPEN. Display them as a table with status and user columns.”

After running git commit && git push && git pull, you will get a new file at boards/issues.md containing something like this:

1# All issues
2
3All open issues
4
5|                                   | status | user            |
6| --------------------------------- | ------ | --------------- |
7| [My title](../issues/my_issue.md) | OPEN   | email@email.com |

Silo has aggregated all your open issues into a single dashboard. Of course, you could easily create another board for closed issues in a separate file like boards/closed.md, it’s entirely up to you.

(Note: If you are sweating at the thought of a plugin scanning your entire repository on every single push, don’t panic. silo, thanks to the plugin-sdk API, doesn’t do a full disk scan. It only processes the incremental diffs generated by the incoming commit. This is precisely why we restrict its scope to the main branch in this example.)

Now that we have a page representing all our issues, we can finally render it in the browser. Because, as you might have guessed, the label causing all this debate lives in the Web UI.

The Web is Where It Happens

To render my boards/issues.md file on the web, I use the apex plugin, in conjunction with the apex_markdown plugin, of course.

These plugins take files from git and render them directly in the Web UI. I configure them like this:

 1- name: apex
 2  ...
 3  run:
 4  - path: "**/*"
 5    branch:
 6    - main
 7    when:
 8    - add
 9    - mod
10    - del
11    func: []
12    write:
13      git:
14      - path: "**/*"
15        can:
16        - add
17        - mod
18        - del
19        - append
20      web:
21      - path: "**/*"
22        can:
23        - add
24        - mod
25        - del
26        - append
27      exec: []
28      callfunc:
29      - pluginname: apex_markdown
30        funcname: renderMd
31      ...
32    configuration:
33      ...
34      menu:
35      - display: 🏠 Home
36        link: /
37      - display: πŸ“– Documentation
38        link: /doc/
39      - display: πŸ”– Versions
40        link: /CHANGELOG.html
41      - display: 🧩 Plugins
42        link: /plugins/
43      - display: 🐞 Issues
44        link: /boards/issues.html
45      - display: πŸš€ Code
46        link: /worktree/
47      - display: β˜• Blog
48        link: /blog/
49      - display: πŸ—¨ Contact
50        link: /contact.html

Oh, do you see it now? Yes, right there: display: 🐞 Issues. This is the exact configuration that tells Apex to add a global menu item with the 🐞 Issues label, pointing to the /boards/issues.html file.

If you are wondering how Apex magically knows where to find your “issues”, the secret is: it doesn’t.

Apex isn’t a smart router, it’s just a file translator. It blindly takes the Markdown structure generated by GitRoot and renders it into HTML. Because I know Apex transforms boards/issues.md into boards/issues.html, I simply hardcode that target URL in my menu configuration (link: /boards/issues.html).

And what about the links inside the table? Apex has a clever built-in rewrite engine. When it parses your generated board file, it automatically rewrites every internal Markdown link, turning [My title](../issues/my_issue.md) into a standard web-friendly <a href="../issues/my_issue.html">. No database, no dynamic routing, just plain old static files mapped one-to-one.

Conclusion

If you change the label to 🐞 Tickets or πŸ’‘ Discussions, the “issue to rename issues into something else” can finally be closed. Ideally, you might also want to rename some directories in the other plugins’ configurations, but it’s really not a big deal.

I’m pretty sure you now understand why I didn’t immediately jump on resolving this task: everything we just saw is pure configuration, completely separated from the GitRoot core code.

And to be perfectly honest, I haven’t changed it yet because none of the traditional options really appeal to me. I don’t particularly like “Tickets” or “Discussions”, maybe it should be “Feature Requests” instead (and yes, you can now model that in GitRoot too!).

In fact, I personally call them TODOs, because in reality, they are all just waiting for me to get them done.

Wait… Why stop at issues?

This is where the true power of GitRoot will blow your mind.

Because GitRoot doesn’t care about databases, and because everything is just a file inside Git, the silo plugin can aggregate anything. I don’t need a separate TODO page for my tickets, because I already have one that automatically tracks the //TODO comments inside my actual source code.

Yes, you read that right. You can visit it live at https://gitroot.dev/boards/todos.html.

Every time I write a //TODO in a .go, .rs or .ts file, GitRoot parses it, aggregates it, and generates a dynamic dashboard. Tickets, documentation, source code comments… it’s all the exact same data structure.

To be clear, it doesn’t magically transform your source code comments into full-blown database-like issues with IDs and statuses. It simply extracts them to give you a centralized, living view of your technical debt, right alongside your official tickets. It’s the exact same data aggregation pipeline, tailored for two different use cases.


If you want to discover more plugins and configurations, feel free to browse through the 🧩 Plugins section. However, for now, the best way to explore remains checking out the actual GitRoot plugins.yml configuration file.