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.
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:
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.
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.
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
issuesdirectory 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!)
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 withstatusandusercolumns.”
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.
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.
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.
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.