Commit Message Conventions
Last updated
May be stale
This page was last updated ago, on 24 September 2026. Check what it says against the code before relying on it.
Internal commits
Internal (non-player-facing) commits are written in Conventional Commits
style: a lowercase type, a colon, then a description — fix: Permanent bans.,
feat: Add migrations library., chore: update photon.. Common types in use here are fix,
feat, chore, docs, refactor, style, tests, and ci. Pick the type that matches what
changed, not what the change was for.
Public Changelogs
Player-facing commits are marked with a public= prefix. Everything from public= onward is
captured as the changelog entry — including any lines after the first, so a commit body written
under a public= subject line is part of the entry, not just the subject line itself.
Where the entry goes
Pull requests currently land as merge commits, and the changelog webhook parses every commit that reaches master as its own entry. The entry goes on the last line of the merge commit message, typed by whoever merges:
Merge pull request 'fix(vehicles): reset FPIS grille bodygroups on None' (#1624) from dev/... into master
Reviewed-on: https://gitea.limelightgaming.net/limelight-development/cityrp2/pulls/1624
Reviewed-by: ...
public=Fixed FPIS grill lights, front and rear sides from duplicating.
The draft lives in a ## Changelog section at the head of the PR body, in a fenced block the
merger copies from. The changelog skill writes it. An internal change has no entry, so its PR
body has no ## Changelog section at all.
Warning
Keep
public=out of the PR title. Gitea quotes the title into the merge subject, and the capture runs to the end of the message, so the entry is published with' (#1416) from dev/... into master Reviewed-on: ...attached.
Warning
Keep
public=off every leaf commit. A merge commit carries each leaf onto master, where it is parsed as an entry of its own, in addition to the merge's. Leaf commits and PR titles use the internal Conventional Commits style above.
Warning
Put
public=last. Anything after it in the same message, a trailer or a second paragraph, becomes part of the entry.
An entry is at most 200 characters, counting everything after public=, hashtags included.
The limit is hard: the database column and the console command that carries the entry both stop
there.
A public= commit looks like:
public=Chefs return to Evocity. Chefs now receive 10% extra EXP when cooking on a stove. #type:update #thread:28936 #thread:28602
Feedback close message
A pull request body has a second, optional public= slot, unrelated to the changelog. When a
merged pull request closes a feedback-synced issue (defined below), the website reads a
close message from the pull request description and shows it to the player on their
Feedback item, attached to its resolved status change:
public=We've added this, should be live soon.
The website reads it by these rules:
- A
public=inside a fenced or inline code block is ignored. - Outside code, the first
public=wins, and everything after it to the end of the body is the message. The allowlisted changelog hashtags (#type:,#thread:and the rest) are stripped. - It only counts when whoever merges holds
feedback.triageon the website.
So a pull request body has two slots, and they never collide:
| Slot | Where | Read by |
|---|---|---|
| Changelog entry | Inside the fence in the ## Changelog section at the head of the body |
The merger, who copies it into the merge commit message |
| Close message | Unfenced, starting at public=, and the last thing in the body |
The website, on merge |
The changelog entry stays inside its fence, which is what keeps the website from mistaking it for
a close message. The close message goes below everything else: Closes #n, the test plan, gate
notes and any tool attribution footer. Anything after it is shown to the player. A pull request
that closes no feedback-synced issue has no close message, and no unfenced public= anywhere in
its body.
A close message runs from a sentence to one paragraph, sized to the request; the changelog skill
drafts it.
An issue is feedback-synced when its body ends with a Synced from: line linking
https://limelightgaming.net/feedback/<id>. Check for that line, not the issue's labels: player
bug reports arrive through the sync with only the repo bug label.
Hashtags
Hashtags are #tag:value, and by convention go at the end of the message, after the player-facing
prose.
| Tag | Meaning | Multiple values |
|---|---|---|
#cmd:text |
A command to highlight | Arbitrary text — /slashcommand and !bangcommand are both fine, it's just displayed as-is. One value per tag; repeat the tag for more than one command. |
#contrib:name |
Credit a contributor | Name or forum user ID. Comma-separated for multiple, and deduplicated case-insensitively. |
#map:name |
Affected map | Resolved to a display name and link. One value per tag; repeat the tag for more than one map. |
#rev:id |
Related revision | One value per tag; repeat the tag for more than one revision — a comma-joined value is not split apart, it just gets treated (and linked) as a single id. |
#thread:id |
Forum thread | Comma-separated values are each turned into their own link. Repeating the tag also works. |
#image:url |
Image | Comma-separated values are each turned into their own thumbnail. Repeating the tag also works. |
#type:value |
Category | Not rendered as its own entry — it picks the accent colour, see below. One value only. |
#type: accepts info, new, update, fix, exploit, or remove.
So #contrib: is the one tag that's reliably comma-separated and deduplicated everywhere it's
read. #thread: and #image: accept comma-separated values too. For everything else
(#cmd:, #map:, #rev:), don't comma-join — repeat the tag once per value instead:
public=... #rev:114 #rev:115 #rev:116 #contrib:name 1,name 2 #cmd:/newcommand
Accent colour
Each changelog entry gets an accent colour — used for its row on the changelog page and for its embed color where it's posted elsewhere. It's picked from keywords anywhere in the captured text (case-insensitive, first match wins, checked in this order):
exploit→ exploitremove→ removebug/fix/hotfix/resolv/recover→ fixadded/new/release/approved→ new- otherwise → the default, update
An explicit #type:value tag overrides whatever the keyword cascade picked.
This means word choice in the description silently affects the accent colour unless #type: is
set explicitly. For example, public=Fixed the new maintenance minigame hits the fix branch
(checked before the new branch), not new, because "Fixed" matches first. If the intended
category doesn't match what the keyword cascade would pick, set #type: explicitly rather than
relying on wording.