vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
|
520470e1
Keenan Nemetz
16d ago
|
1 | # Issues |
| 2 | ||
| 3 | Your issues belong to the repository, not to a vendor. They clone with the code, | |
| 4 | keep working when a hosting service has an outage, and stay readable in any text | |
| 5 | editor. | |
| 6 | ||
| 7 | An issue is a Markdown file stored on a special branch called `vole`. Every clone | |
| 8 | of the repository carries the issues and their history, so there is no separate | |
| 9 | issue database to back up. | |
| 10 | ||
| 11 | ## Where issues live | |
| 12 | ||
| 13 | ```text | |
| 14 | issues/<ulid>/issue.md # the issue | |
| 15 | issues/<ulid>/comments/<ulid>.md # a comment | |
| 16 | ``` | |
| 17 | ||
| 18 | Here is a complete issue: | |
| 19 | ||
| 20 | ```markdown | |
| 21 | --- | |
| 22 | title: "Nothing renders on the docs page" | |
| 23 | state: open | |
| 24 | author: you | |
| 25 | created: 2026-10-22T21:30:00Z | |
| 26 | labels: | |
| 27 | - bug | |
| 28 | --- | |
| 29 | Steps to reproduce... | |
| 30 | ``` | |
| 31 | ||
| 32 | The top section, between the `---` lines, is YAML metadata. The rest is the | |
| 33 | Markdown body. Titles and other free text are always quoted, so characters like | |
| 34 | `#` or a leading `-` cannot change the meaning of the file. | |
| 35 | ||
| 36 | An issue id is a 26-character ULID. It uses uppercase letters and digits, but not | |
| 37 | `I`, `L`, `O`, or `U`. A UUID will not work. | |
| 38 | ||
|
e05f1c48
Keenan Nemetz
13d ago
|
39 | ## Numbers |
| 40 | ||
| 41 | Nobody types 26 characters, so every issue and change request also has a small | |
| 42 | number: `#1`, `#42`. Issues and change requests share one sequence, oldest first, | |
| 43 | the way one forge counts both in a single run. | |
| 44 | ||
| 45 | The number is **derived, not stored**. It is the position of the commit that | |
| 46 | created the entity in the metadata branch's history. Nothing is added to the file, | |
| 47 | no counter is kept, and there is nothing to migrate: an existing repository gets | |
| 48 | its numbers the first time anyone asks. Two clones of the same history produce the | |
| 49 | same numbers, and `vole admin index rebuild` reproduces them from git. | |
| 50 | ||
| 51 | The ULID is still the identity — it is the path, the front matter's neighbour in | |
| 52 | `closes:`, the thing in the commit messages — so a number is a convenience, not a | |
| 53 | reference you should quote in a script. The one way to make one wrong is to rewrite | |
| 54 | the branch history: an admin redaction force-push renumbers everything after the | |
| 55 | commit it rewrote. | |
| 56 | ||
| 57 | Every command that takes an id takes a number instead, with or without the `#`: | |
| 58 | ||
| 59 | ```sh | |
| 60 | vole issue show 42 | |
| 61 | vole issue close '#42' | |
| 62 | vole change show 1 | |
| 63 | ``` | |
| 64 | ||
|
520470e1
Keenan Nemetz
16d ago
|
65 | ## Writing an issue without the CLI |
| 66 | ||
| 67 | ```sh | |
| 68 | git switch --orphan vole | |
| 69 | ID=01M5JR2JK29KKMAA1T9MDNTPBP | |
| 70 | mkdir -p issues/$ID | |
| 71 | cat > issues/$ID/issue.md <<'MD' | |
| 72 | --- | |
| 73 | title: "Nothing renders on the docs page" | |
| 74 | state: open | |
| 75 | author: you | |
| 76 | created: 2026-10-22T21:30:00Z | |
| 77 | labels: | |
| 78 | - bug | |
| 79 | --- | |
| 80 | Steps to reproduce... | |
| 81 | MD | |
| 82 | git add -A | |
| 83 | git commit -m "issue: docs page blank" | |
| 84 | git push origin vole | |
| 85 | ``` | |
| 86 | ||
|
7e0b5950
Keenan Nemetz
6d ago
|
87 | Vole checks the file when you push. The `author` field must be the account you push |
| 88 | as unless you hold `maintain`, which is the one place the browser is stricter than a | |
| 89 | push: a contributor writes as itself. If someone else pushed first, the CLI puts your | |
| 90 | change on top of theirs and tries again. | |
|
520470e1
Keenan Nemetz
16d ago
|
91 | |
|
6c84d387
Keenan Nemetz
16d ago
|
92 | ## From the browser |
| 93 | ||
|
ab326d3a
Keenan Nemetz
14d ago
|
94 | When browser sign-in is enabled (see [Accounts and access](accounts-and-access.md#browser-sign-in)), |
|
6c84d387
Keenan Nemetz
16d ago
|
95 | a signed-in contributor or maintainer can open an issue or comment from the |
| 96 | repository's issue page. The author is the account whose passkey opened the | |
| 97 | session; the form cannot choose a different name. The change is written with the | |
|
7e0b5950
Keenan Nemetz
6d ago
|
98 | same metadata writer the CLI uses, so it is the same file on the same branch. A |
| 99 | contributor can use this form or the CLI; pre-receive asks the same capabilities the | |
| 100 | form does, so neither offers more than the other. | |
|
6c84d387
Keenan Nemetz
16d ago
|
101 | |
|
802e0e52
Keenan Nemetz
5d ago
|
102 | The issue's author or a maintainer can correct its title with the pencil beside |
| 103 | the heading, or its description from the card's menu. Each opens its own field in | |
| 104 | place, so saving does not leave the page or replace the discussion. The discussion, | |
| 105 | labels, and assignees are left alone, and the edit is an ordinary metadata commit | |
| 106 | like the one the CLI writes. | |
|
a4e7a99a
Keenan Nemetz
6d ago
|
107 | |
|
520470e1
Keenan Nemetz
16d ago
|
108 | ## Using the CLI |
| 109 | ||
| 110 | ```sh | |
| 111 | vole issue new --title "Nothing renders on the docs page" -m "Steps..." -l bug -a wilbur | |
| 112 | vole issue comment -m "Also affects search" 01M5JR2JK29KKMAA1T9MDNTPBP | |
| 113 | vole issue edit-comment 01M5JR2JK29KKMAA1T9MDNTPBP 01M5N1PRC4GJPZNEEG8PR8J4D6 -m "Corrected." | |
| 114 | vole issue rm-comment 01M5JR2JK29KKMAA1T9MDNTPBP 01M5N1PRC4GJPZNEEG8PR8J4D6 | |
| 115 | vole issue edit 01M5JR2JK29KKMAA1T9MDNTPBP --title "A better title" -l bug -l docs | |
| 116 | vole issue assign 01M5JR2JK29KKMAA1T9MDNTPBP wilbur | |
| 117 | vole issue unassign 01M5JR2JK29KKMAA1T9MDNTPBP wilbur | |
| 118 | vole issue close 01M5JR2JK29KKMAA1T9MDNTPBP | |
| 119 | vole issue reopen 01M5JR2JK29KKMAA1T9MDNTPBP | |
|
e05f1c48
Keenan Nemetz
13d ago
|
120 | vole issue list --state open -l bug --assignee wilbur --format '%(number) %(title)' |
|
520470e1
Keenan Nemetz
16d ago
|
121 | vole issue show 01M5JR2JK29KKMAA1T9MDNTPBP # a prefix is enough |
|
e05f1c48
Keenan Nemetz
13d ago
|
122 | vole issue show 42 # the number is enough |
|
520470e1
Keenan Nemetz
16d ago
|
123 | ``` |
| 124 | ||
|
e05f1c48
Keenan Nemetz
13d ago
|
125 | `--format` understands `%(number)`, `%(id)`, `%(shortid)`, `%(state)`, `%(author)`, |
| 126 | `%(title)`, `%(labels)`, `%(assignees)`, and `%(created)`. `%(shortid)` is a | |
| 127 | prefix of the ULID, which is mostly timestamp; `%(number)` is the alias people | |
| 128 | read and quote. | |
|
520470e1
Keenan Nemetz
16d ago
|
129 | |
| 130 | The CLI commits and pushes for you. It uses the account your SSH key belongs to, | |
| 131 | not the name in `git config`. If you want to commit without pushing, add | |
| 132 | `--no-push`. | |
| 133 | ||
| 134 | You can also create an issue in a new directory with no remote configured: | |
| 135 | ||
| 136 | ```sh | |
| 137 | git init bug-report | |
| 138 | cd bug-report | |
| 139 | vole issue new --title "the docs page is blank" \ | |
| 140 | --remote ssh://git@forge.example.com/demo.git | |
| 141 | ``` | |
| 142 | ||
| 143 | ## Rules | |
| 144 | ||
|
b492b557
Keenan Nemetz
3d ago
|
145 | - Issues and comments live under `issues/` and `changes/` on the `vole` branch. |
|
b312e166
Keenan Nemetz
11d ago
|
146 | Other paths are rejected. Releases have their own paths, written by Vole. CI |
| 147 | records are not stored on the branch; they live in `data/runs.db`. | |
|
520470e1
Keenan Nemetz
16d ago
|
148 | - History on the `vole` branch must be linear. Merge commits are not allowed. |
| 149 | - Force-pushing `vole` requires repository admin and is recorded in | |
| 150 | `data/audit.log`. It is meant for redaction, not normal work. | |
|
e05f1c48
Keenan Nemetz
13d ago
|
151 | - Writing `#42` in a body links to that entity. Because one sequence holds both |
| 152 | kinds, the link goes through `/n/42`, and the server resolves it to the issue or | |
| 153 | the change request that holds the number. `#<ulid>` links by id and is the form | |
| 154 | that keeps working if history is ever rewritten. Commits that mention an id are | |
|
520470e1
Keenan Nemetz
16d ago
|
155 | shown as backlinks on the issue page. |
|
a6b42433
Keenan Nemetz
8d ago
|
156 | - Writing `@name` links to that account's profile. It is the same syntax a |
| 157 | workflow's `mentioned` filter matches, so the page and a job agree about who was | |
| 158 | addressed. Code samples are skipped, and a name that is not an account stays | |
| 159 | text. | |
|
520470e1
Keenan Nemetz
16d ago
|
160 | - An assignee must be a valid account name: lowercase letters, digits, `-`, and |
|
474a812d
Keenan Nemetz
16d ago
|
161 | `_`. The name is stored as written because account names are never reused. A |
|
62576fa8
Keenan Nemetz
14d ago
|
162 | value that is not an account name cannot be an assignee. |
|
520470e1
Keenan Nemetz
16d ago
|
163 | - The web page can filter by state, label, and assignee, and search titles and |
| 164 | bodies. | |
| 165 | ||
| 166 | ## Comments | |
| 167 | ||
|
f6f4b1db
Keenan Nemetz
15d ago
|
168 | A comment is a file at `issues/<id>/comments/<ulid>.md` with `author`, |
| 169 | `created`, and optionally `reply_to` metadata. `reply_to` names any comment in | |
| 170 | the same thread. The web renders one level of nesting by collecting a root's | |
| 171 | descendants beneath it. | |
|
520470e1
Keenan Nemetz
16d ago
|
172 | |
| 173 | Only the author of a comment can edit or delete it. Vole compares the author on | |
| 174 | the old version of the file with the account behind the SSH key. An admin can | |
| 175 | override this, for example to repair a thread after someone leaves, and the | |
| 176 | override is recorded in `data/audit.log`. | |
| 177 | ||
|
f6f4b1db
Keenan Nemetz
15d ago
|
178 | Deleting a comment replaces it with a tombstone ("Comment deleted") so replies |
| 179 | keep the thread they answer. The text remains in Git history, and a deleted | |
| 180 | comment cannot be edited. Editing a comment does not add an "edited" field; the | |
| 181 | commit carries the time. Deleting is not redaction. Redaction means an admin | |
| 182 | force-push. | |
| 183 | ||
| 184 | The web comment menu offers Reply, Copy, Edit and Delete. Editing and deleting | |
| 185 | are limited to the author or an admin, and deletion leaves a tombstone so replies | |
| 186 | keep their thread. | |
|
520470e1
Keenan Nemetz
16d ago
|
187 | |
| 188 | The issue itself works differently. Closing, relabeling, or assigning someone | |
| 189 | else's issue is normal triage, not a claim of authorship. |