.git-board.toml¶
Overview¶
The file is optional. Without it, every ns:value label is a field and git-board works on any git repository. The file adds what labels can't say: the order of a field's values, single or multiple values, colors, sprint dates, saved views, the GitHub Project to sync with, and where attachments go.
It lives at the root of the repository. Edit it in Settings → Project file: it is checked as you type and applies as soon as you save. You can also edit it by hand. The running board reads it again within a second. If the file does not open, the board keeps the last file that opened, and the web app shows the error.
A repository without one starts from the default: Write the default file in Settings › Fields or on the Start page (#/start), or git board init in a terminal (it never overwrites a file). The default declares a status workflow (backlog, ready, in-progress, in-review, ready-for-validation, validated), priorities p0 to p3, an on-hold field beside the status (hold:blocked, hold:paused), ten kinds, GitHub's plain labels (bug, feat, … chore, design) with their descriptions, each the prefix of its branches (bug/vm-12-login-fails), estimates in hours, MoSCoW, sprints of 14 days, and four views (all, backlog, board, mine), each explained in a comment.
A new board: its forge, then a template¶
git board init starts the file from a template, then, when origin is on GitHub or GitLab, from the forge itself, so the two agree on the first day (Settings › Labels shows nothing to adopt):
- The template.
--templatenames it:enac(built in: EPFL-ENAC's labels and GitHub Project, values spelled as GitHub has them,P0,Must), a file, or anhttp(s)URL. Without the flag, the global file'stemplate; without that, the default above. - The forge. With the token a sync uses (
GITHUB_TOKENfor github.com,GITLAB_TOKENforGITLAB_HOST, else git's credential helper: ADR 0010), init reads origin's labels and, on GitHub, its first Project: their names, colors and descriptions, and each Project field's values in order. They win over the template, which fills only what the forge lacks. A label<field>:<value>(hold:blocked,prio:P0) is a value of that field, a kind's label (bug) colors the kind, any other is a plain label in[labels]. The Project goes in[github-project], so its fields' labels are held there, not created; with more than one, the first, and a line saying so. GitLab has no Projects: its labels alone, and a line saying so. No token, or nothing read: the template, and a line saying what wasn't read.
The global file is ~/.config/git-board/config.toml ($XDG_CONFIG_HOME/git-board/config.toml when it is set):
It only starts new boards: a repository with a .git-board.toml never reads it.
A template names itself in a [template] table, which init copies:
git board lint then lists the drift (template): what the built-in template has that the file lacks (a field, a value) and a newer version. What the file has more, or spells or colors as its forge does, is the project's, and so are the values of a field its [github-project] holds. A file or URL template isn't compared.
The file's first key, schema = 2, is the version of its format, which
git-board upgrades and downgrades: The file's schema version.
| Table | What it sets |
|---|---|
[fields.<name>] |
a field's type, values, order, single or multiple: together, the project's ontology |
[fields.<name>.colors] |
the color of each value |
[fields.<name>.descriptions] |
what each value means, as its GitHub label says it |
[views.<name>] |
a saved view: layout, filter, grouping, sort |
[sprints], [[sprints.list]] |
sprint length and dates, for sprint:@current |
[github-project] |
the GitHub Project to sync fields with |
[uploads] |
where attached files go |
[branches] |
the branch an issue suggests: its base and its name |
[cycle], [cycle.signoffs] |
the development cycle: which statuses are which stage, where plans live, which files are tests, who holds each gate |
[template] |
the template the file came from, and its version, for lint's drift |
[repos] |
the GitHub repositories the project spans, and how the forges hold assignees |
Fields¶
These are the forms of Settings › Fields; this section is the reference.
A field is a label namespace: status:ready is the value ready of the field status. Every namespace found on the issues is already a field; declare one here to give it an order, a cardinality or colors.
[fields.status]
single = true
options = ["backlog", "ready", "in-progress", "in-review", "validated"]
[fields.est]
single = true
numeric = true
options = ["0.5", "1", "2", "4", "8"]
[fields.assignee]
single = false
| Key | Type | Default | What it does |
|---|---|---|---|
options |
list of strings | none | The values, in workflow order: board columns, sort order, and the next status for git board move <id> --next; the last one is what git board close sets. A value outside the list still works; git board lint reports it. |
single |
true / false | false | One value at a time: setting a new one replaces the old. Multiple values (false) suit assignees or modules. |
numeric |
true / false | false | Sorts as numbers. A field named est shows its values in hours (3h). |
project |
inline table | none | The GitHub Project's option name for a value, when it isn't the value: project = { comite = "Comité Spécialisé" }. |
type |
one of the types below | guessed | What the values are; see Types. |
unit |
string | none (h for est) |
Written after a number field's values: unit = "pt". |
Types¶
These are the forms of Settings › Fields; this section is the reference.
The fields and their types are the project's ontology: the kinds of things an issue can say about itself, and how they relate. An assignee is a person, a sprint is a span of dates, a parent is another issue. The type decides how a field sorts, filters, shows and is edited; its values stay labels (due:2026-10-30, parent:#12), so they travel to GitHub and GitLab like any label.
| Type | For | What it gives | Guessed from |
|---|---|---|---|
select |
status, priority, module | ordered values (options), colors, board columns |
options; any field nothing else fits |
iteration |
sprint | the dates of [[sprints.list]], @current, sorting by Dates, today's sprint tagged |
the names sprint, iteration |
person |
assignee, reviewer | logins, @me, several at once |
the names assignee, assignees, reviewer, owner |
number |
estimate, points | numeric sorting, a unit after the values |
numeric = true |
date |
due date, deadline | a date picker, red once past, amber within a week, "3 days late" | the names due, deadline; values that are all YYYY-MM-DD |
issue |
parent, blocked-by | a link to the other issue, and on it the reverse: Sub-issues, Blocks | the names parent, blocked-by, blocks, depends-on, duplicate-of; values that are all #12 or issue ids |
text |
anything free | a plain text box | only when set |
In Settings › Fields the form follows the type: a select lists its values in order, a number its unit, and a date, a person, an issue or a sprint has no values of its own. Saving such a field takes its options out of the file, with their colors and descriptions.
Set a type when the guess is wrong, or for a field no issue uses yet:
[fields.due]
type = "date"
[fields.points]
type = "number"
unit = "pt"
[fields.parent]
type = "issue"
An issue field names the other issue by its forge number, #12, or by its id, at least its first 7 characters.
On hold is a field beside the status, never a status: the default's hold (blocked, paused) leaves a blocked issue at its place in the workflow. An issue without one isn't missing a value.
Colors¶
These are the forms of Settings › Fields and Labels; this section is the reference.
Every field value already has a color, picked from words in it: done, validated or closed are green; review or qa purple; validation pink; in-progress, doing or wip blue; ready or feat teal; waiting, comite or p2 amber; test, p1 or high orange; blocked, bug, fix or p0 red; backlog, todo, p3 or low grey. Other values take blue, purple, teal and pink in turn. Numbers and assignees stay plain.
Pick a color yourself for any value:
The colors are grey, blue, purple, amber (or yellow), orange, green, red, teal and pink, or a hex "#d73a4a", as a GitHub label has it. A hex color paints the value's dot; its text stays the page's. Another name stops the file from loading, and the editor says which line. Quote a value that has spaces or other punctuation than - and _.
A value's description is what its label says on GitHub, 100 characters at most (GitHub's limit):
Labels¶
These are the forms of Settings › Labels; this section is the reference.
[labels] lists the plain labels the project allows besides its fields: yes-or-no flags that can stack and that no field holds, such as needs-design or regression. A label an issue can carry only one of (a priority, a kind, a team) is a field's option instead.
[labels]
list = ["needs-design", "regression", "security"]
[labels.colors]
regression = "red"
[labels.descriptions]
needs-design = "waits for a design decision"
| Key | Default | What it does |
|---|---|---|
list |
none: nothing to pick | The labels, in the order the issue's head offers them as checkboxes, never as free text. A label with a colon (prio:p1) is a field's value and a kind is the kind field's: both are refused. |
colors |
the palette, in turn | A label's color: a palette name or #rrggbb, as in [fields.<name>.colors]. |
descriptions |
none | A label's description on GitHub, 100 characters at most. |
With the table, git board lint reports an open issue's plain labels outside the list (label-outside); an empty list allows none. Without it, lint says nothing about labels and the issue's head has nothing to pick: a label already on an issue shows with a warning.
Views¶
These are the forms of Settings › Views; this section is the reference.
A view is a saved page: a tab above the issues, in the file's order (git-board opens on the first), and its address can be bookmarked. Table, board or roadmap is a switch beside the tabs, on any view; layout is the one it opens in.
You rarely need to write one by hand. On the board, change the filter (its box, or a tick in the filters panel), the layout, the grouping or the sort, and a bar offers Save view (into this view, for everyone) or Save as new view…; Discard goes back to the saved one. Only the view's table changes in the file, comments included.
[views.current-sprint]
layout = "board"
filter = "sprint:@current is:open"
group = "status"
[views.backlog]
layout = "table"
filter = "is:open no:sprint"
sort = "prio asc"
[views.roadmap]
layout = "roadmap"
filter = "is:open"
group = "status"
| Key | Type | Default | What it does |
|---|---|---|---|
layout |
"board", "table" or "roadmap" |
"table" |
A board has a column per value of group, and cards move between columns by drag and drop. A table has a row per issue. A roadmap puts each issue on a time line: a bar over its sprint's dates, a diamond on its date fields (due), today marked; issues with neither are listed below it, unscheduled. |
filter |
string | is:open |
Which issues show; see Filters. |
group |
field name | none | The board's columns, or the table's sections: status, sprint, any field. |
sort |
"<field> asc" or "<field> desc" |
none | Order within a column or section: by the field's options order, numerically for a numeric field, else alphabetically. |
columns |
list of strings | the default ones | A table's columns, in order: id, title (without a trailing (#2518) that is the issue's own number: links shows it), is (open or closed), any field, links (the issue on each forge), other (labels without a namespace). Without it, the table shows id, forge links, title, state, every field and other, so a new field gets its column. Set from the table's Columns button and Save view. |
lanes |
list of values | every value | A board's columns: which values of group it shows, in this order; "(none)" for the issues without one. The other issues are left out, and the board says how many. Set from the board's Lanes button. |
limits |
inline table | none | At most this many cards in a lane, work in progress: limits = { in-progress = 5 }. The lane shows "4 / 5", amber at the limit, red over it. |
Boards per role¶
The same issues, a board for each way of working: each view keeps the lanes its people move cards between.
[views.dev]
layout = "board"
group = "status"
lanes = ["ready", "in-progress", "in-review", "ready-for-validation"]
limits = { in-progress = 5 }
[views.pm]
layout = "board"
filter = "is:open pm:@me"
group = "status"
lanes = ["(none)", "backlog", "comite", "ready", "qa"]
[views.qa]
layout = "board"
group = "status"
lanes = ["comite", "ready", "qa", "tests-metier", "validated"]
On a board, Cmd- or Ctrl-click picks several cards (Shift-click a range in a lane, or the box on a card's corner), and dragging one of them moves them all; a lane's ⋯ menu moves every card it holds.
Roles¶
These are the forms of Settings › Roles; this section is the reference.
A project's people by role, as the team's roles page names them: the lead developer, the project manager, the code reviewer. Every issue takes them, unless it sets its own.
A role is a person field of the same name: lead-dev:dan on an issue overrides the project's bob for that issue, and removing it gives bob back. Filters, boards and the filters panel see the inherited value: lead-dev:@me finds the issues that take you as lead dev, the PM's board above (pm:@me) every issue while the project names them PM, and no:lead-dev finds none while the project names a lead dev. The side panel shows an inherited value greyed, "bob · project". assignee is the contributing developer, set per issue.
The inbox brings a role you hold for the project only the project's new issues and closes, not their discussions: the PM of a project isn't in every comment. A role set on the issue itself (lead-dev:dan) follows it like an assignee does. involves:@me and follows:@me count the issue's own roles, not the project's.
| Key | What it is |
|---|---|
a role (lead-dev, pm, reviewer, …) |
One login. The name is a field's: lower-case letters, digits and -; declared in [fields], it is a person. |
Filters¶
These are the forms of Settings › Views (the filter syntax, beside the form); this section is the reference.
One syntax everywhere: a view's filter, the board's Filter box, ⌘K, and the command line (git-board ls, git-board tui and its f). Conditions combine with AND, as in GitHub's search; a leading - negates any of them.
| Syntax | Matches |
|---|---|
status:ready |
issues with the label status:ready |
-status:ready |
issues without it |
status:ready,qa |
either value, as in GitHub's search; works with is:, label: and author: too |
sprint:@current, @next, @previous |
the sprint of today, the next or the previous one, from [[sprints.list]] |
assignee:@me |
you: the person you write as (git board me), by their GitHub or GitLab login or by their name. Without a forge account, assignee=+@me writes the name (assignee:Ada Lovelace); a sync sends it as their login where they have an account |
is:open, is:closed |
open or closed issues |
title:*spec* |
a title matching the pattern, without case: * any text, / included, ? one character, [a-z] and [!a-z] a class (a [ that closes none is a [) |
author:guilbep, author:@me |
who opened the issue |
involves:guilbep, involves:@me |
issues someone is in: the author, a person field (assignee, reviewer…), a comment, an @mention |
follows:@me |
what your inbox follows: involves:@me, the issues you followed and the views you follow, without those you unfollowed |
label:bug |
a plain label, or a full one: label:status:ready; label:bug also finds the kind bug when the kinds are plain labels (see Branches) |
no:assignee |
no value for that field; no:label: no plain label (a plain kind is the kind field, not one) |
#2528 |
the issue with that number on a forge |
travel, "car sharing" |
words in the title, the description or a comment; a number (2528), or the first 7 characters or more of an id, also finds that issue |
in:title, in:body, in:comments |
where the words are looked for: crash in:title; in:title,body either; -in:comments anywhere but there |
created:2026-10-01 |
opened that day, in local time (of the machine that answers: yours on the command line) |
created:>2026-10-01, >=, <, <= |
opened after that day, from it, before it, up to its end |
created:2026-10-01..2026-10-31 |
opened in that span, both days in; 2026-10-01..* from that day on |
created:>=@today-7d |
relative to today: @today, then - or + a count of d days, w weeks, m months or y years |
created:>2026-10-01T14:30 |
a time: that minute; with seconds, or a zone (2026-10-01T14:30:00Z), that second |
updated:<@today-30d, closed:2026-10-01..* |
the last change; the last close (an open issue has none) |
comments:>5 |
how many comments: 5, >5, >=5, <5, <=5, 2..5 |
Words match the start of words, without case or accents: crash finds
"Crashes". A date, a count or an in: that isn't one is an error, not
an empty list. @today is the day of the board you look at: with git
board ls --at, the day you went back to.
An empty filter is every issue, open or closed. A view without one shows the open issues (is:open), as git-board ls does without one.
On the board, the Filter box holds the view's filter until you change it; the address then keeps it (?view=backlog&filter=…), so a link shows the same issues. The filters panel counts each field's values against the rest of the filter, and a tick writes into the box: ready and qa ticked is status:ready,qa, Not set no:status, Not set with values the values left out (-status:validated). ⌘K takes the same filter on every issue, the open ones first, and its first row, Filter the board by, puts it into the box; #2528 or an id opens that issue at once. Typing status: in either offers the field's values. On the command line, quote a filter: git-board ls "#2528" (the shell takes an unquoted # for a comment).
Sprints¶
These are the forms of Settings › Sprints; this section is the reference.
Sprints are a field like any other (sprint:s12). The list gives them dates, so that sprint:@current finds today's sprint.
[sprints]
length_days = 14
[[sprints.list]]
slug = "s12"
title = "Sprint 12"
start = 2026-10-05
[[sprints.list]]
slug = "s13"
title = "Sprint 13"
start = 2026-10-19
| Key | Type | What it does |
|---|---|---|
length_days |
number | The length of the last sprint, whose end has no next start; without it, as long as the sprint before it, else 14 days. Once the last sprint has ended, @current is no sprint. Also what + Sprint adds to the last start. |
slug |
string | The label's value: sprint:s12. |
title |
string | The name shown. |
start |
date, YYYY-MM-DD, no quotes |
First day. A sprint lasts until the next one starts. |
+ Sprint adds the next one after the last: its start plus length_days, its number plus one.
The sprint field follows these dates: in the filters (sorted by Dates, today's sprint tagged current), on a board grouped by sprint, in a table's sections and when sorting. In an issue, the sprint is picked from the list. Give [fields.sprint] its own options to choose another order.
GitHub Project¶
The GitHub Project whose fields git board project pull, push and sync turn into labels and back, by the Project's field names: Status (status:), Priority (prio:), Sprint (sprint:, an iteration field), Est. hours (est:, a number), MoSCoW (moscow:), Module (mod:), and Milestone (milestone:, its title as a slug), which is read only: the issue's own, a deliverable, set on GitHub. An option whose name doesn't slug to the value is named in the field's project table, as the default file does for MoSCoW's "Won't". The whole sync, Settings › Sync and git board sync, runs git board project sync as its third step: after the forges, before the git push.
Settings › Sync sets it up, on the Project's line: a number, a Project of the GitHub forge's owner, or the Project's address (https://github.com/orgs/acme/projects/40), checked as soon as it is saved; another Project than the one written to is only read, until write = true is set again. git board project configure --owner acme --project 40 writes the same.
| Key | Type | Default | What it does |
|---|---|---|---|
owner |
string | none | The user or organization owning the Project. |
number |
number | none | The Project's number, as in its address. |
write |
true / false | false | Lets a sync write git-board's changes to the Project. Off, the Project is only read. |
unmapped |
table | none | The Project's values to leave out, by field: [github-project.unmapped], category = ["SPECS"], status = ["Icebox"]. A sync skips them and warns of nothing. |
The Project's Category fills an issue's kind: FEAT is the kind feat, in any case. A Category that is a kind in another word is named in [fields.kind.aliases], FIX = "bug": with plain-label kinds, an alias folds into one of [branches] kinds.
[fields.kind.aliases]
FIX = "bug"
DOC = "docs"
[github-project.unmapped]
category = ["SPECS", "TEXT"]
git board project fit, and Settings › Sync after a check, propose these keys for a Project that came before the file: the options a status lacks (with their name in project when it doesn't slug to the value), the aliases, and a [[sprints.list]] entry with its start date for each iteration (project fit).
git-board reads and writes the Project through GitHub's GraphQL API with the GitHub forge's token (Forges › Tokens): it needs the read:project scope to read the Project, project to write it.
Forges¶
The forge projects the issues sync with, GitHub or GitLab, one table each, named as you like: Settings › Sync adds, switches and removes them. Forges says how a sync goes. With no table, the forges are those origin is on, its fetch and push URLs, the push-only ones with their sync off (Forges); one table or more, or discover = false, turns that off.
[forges]
publish-private = false
[forges.gh]
kind = "github"
project = "guilbep/git-board"
login = "guilbep"
sync = true
[forges.epfl]
kind = "gitlab"
host = "gitlab.epfl.ch"
project = "42400"
login = "guilbep"
sync = true
[forges.backup]
kind = "gitlab"
host = "gitlab.example.com"
project = "42"
sync = true
way = "push"
| Key | Type | Default | What it does |
|---|---|---|---|
publish-private |
true / false | false | Lets what came from a project that isn't public go to one that is. An internal project (GitLab's, or a GitHub Enterprise's) counts as public when it receives, and as not public when it sends. |
discover |
true / false | true | With no [forges.<name>] table, finds the forges in origin's URLs. A table turns it off whatever it says. |
kind |
github or gitlab |
none | The forge. |
project |
string | none | GitHub: owner/repo. GitLab: the project's numeric id, on its page under its name, which a renamed group doesn't change. |
host |
string | github.com, gitlab.com |
The forge's host, for GitHub Enterprise or a GitLab instance. |
login |
string | none | The posting account: who sync writes as, for the people without a token there. |
sync |
true / false | false | Takes part in the sync. Off, the forge is kept but left out. |
way |
push, pull or both |
both |
The mirror's direction (ADR 0008). push: the issues go there, and nothing is pulled from it — a backup (The way of a mirror). pull: read-only, the sync reads its issues and writes nothing there. |
No token goes in this file, which is committed. A token comes from GITHUB_TOKEN (github.com only), GITHUB_ENTERPRISE_TOKEN (the host GITHUB_ENTERPRISE_HOST names) or GITLAB_TOKEN (the host GITLAB_HOST names, gitlab.com by default), else from git's credential helper for the host and login: git credential fill. A helper with nothing for that login is asked once more for the host alone, as a keychain keeps a host's token without a username; a person's token counts only when the forge says it is theirs.
Where a field goes on a forge¶
In the board, every field is a label. On a forge, a field goes where its mapping says, else to its type's default on that forge's kind (ADR 0008, section 5).
to is one of label, scoped-label, project, assignees, milestone, iteration, weight, due-date, sub-issues or none. A field with no mapping takes its type's default:
| Type | GitHub | GitLab |
|---|---|---|
person, field assignee |
native assignees | native assignees |
issue |
sub-issues | label |
iteration |
label | milestone |
date |
label | due date |
text |
none (git-board only) | label |
| any other type | label | label |
On GitHub with [github-project], the fields that its fields key names go to the Project's columns.
The sync writes labels and native assignees only. A label is held on a forge unless its field goes to a label or a scoped label there: the sync never sends it, never pulls it and never removes it. A milestone: label is always held. The assignees go to the forge's own assignees, and the Project's fields go to its columns. A field with any other home stays in git-board alone on that forge. Settings › Fields shows where each field goes on each forge.
Attachments¶
These are the forms of Settings › Attachments; this section is the reference.
Files attached to comments go to an S3 bucket that a CDN serves; the comment gets a link to them. The keys stay out of this file: set ACCESS_KEY_ID, ACCESS_KEY_SECRET and BUCKET_NAME where the git-board server (git board, git board serve) runs. Without them, attaching is off.
| Key | Default | What it does |
|---|---|---|
endpoint |
https://s3.epfl.ch |
The S3 service. The keys are the server's, so is where they go: it must be the server's UPLOADS_ENDPOINT (else https://s3.epfl.ch), or attaching is off. |
public_url |
https://enacit4r-cdn.epfl.ch |
Where the bucket is served. |
prefix |
git-board/<repository> |
The folder in the bucket; a .. in it turns attaching off. |
region |
us-east-1 |
The S3 region. |
Files are public to anyone with the link, even for a private repository, and stay cached on the CDN for up to a year after a delete.
A file is 25 MB at most, GitHub's own limit. Its key in the bucket is <prefix>/YYYY-MM/<random>/<name>: the bucket is not browsable, so the random part is the only protection. The defaults are EPFL ENAC's. On another install, set [uploads] and the server's UPLOADS_ENDPOINT. git board datastore copy and the Markdown export do not copy the files: they stay in the bucket, linked (ADR 0002, section 7).
Branches¶
These are the forms of Settings › Branches; this section is the reference.
An issue's Development section lists the branches whose name names it: merged into dev when the branch's last commit is in dev, in dev, squashed when its changes are there by a squash or a rebase (what git merge-tree makes of the two is dev as it is), else not in dev. Merging with merge commits (--no-ff) keeps the first one exact. Create branch gives the command creating its branch (git board branch <id> prints the same). git-board creates and pushes no branch.
When origin is on GitHub, each of these branches that origin has carries its gate: the pull request whose head it is, found on GitHub by the branch's name (never stored), its checks (the base's required ones first, from its rulesets, a required check that hasn't reported yet included), who approved and at which commit, and GitHub's review decision. Merge into dev is enabled when GitHub says the pull request can merge, and otherwise says what it waits for ("Frontend Tests, an approval from alice"); it merges with a merge commit, and only if the head is still the commit the page showed. Open PR opens one into the base, titled CO2-3075 <title>, its body the trailer naming what it delivers, Refs: CO2-3075 (Refs: #<its number> for one of origin's GitHub issues with no project number), to which you add the criteria's letters (Refs: CO2-3075 A,B). Never a closing keyword: merging doesn't finish the issue. A branch only here shows the git push -u origin <name> to copy. The page asks GitHub again every 15 seconds while a check runs.
Open PR and Merge act on GitHub as you: with your GitHub login (your person's github.com account) and its token, GITHUB_TOKEN when set, else git's credential helper's for github.com and that login. Without one, the gate says no GitHub token for <login> and uses no one else's. It is the account sync posts your comments under, not the gh CLI's, which can be logged in as someone else: GitHub refuses an approval from a pull request's author, so who opens and who approves must be told apart.
The full issue page has a Changes tab: the branch from where it left the base (the merge base with origin/<base>) to its head, origin's copy when origin has the branch, since that is what the pull request and its reviewers see. A picker shows all of it, what changed since your last review, or one commit. Files come in three groups: To review (those under 1,000 changed lines open by themselves), Large (over 3,000 changed lines, folded, the first 5,000 lines when opened, and the git diff to copy), and Generated (linguist-generated in .gitattributes, as on GitHub: folded and left out of the viewed count). Viewed marks are kept in this browser, per file version: a push un-views only the files it changed. Click a line number to comment on that line; a comment shows under its line, quoted in the conversation with its path:line, and outdated once a later commit changed that line (it then stays above the file's diff). Replies and Resolve work on the thread; Comment, Approve and Request changes submit a review at the head, and each person's last verdict shows with how many commits ago it was. All of these are issue comments carrying review:* metadata (path, line, side, commit, the pull request, the comment answered, the verdict, resolved), so they are in the repository at once. They stay in git-board: the forge sync never posts a comment with review:* metadata, to an issue or to a pull request.
[branches]
base = "dev"
name = "{kind}/{number}/{n}-{slug}"
kinds = { bug = "bug", feat = "feat", perf = "perf", chore = "chore" }
| Key | Default | What it does |
|---|---|---|
base |
the remote's default branch, else main, master or trunk |
The branch a new branch starts from (origin/<base>, or <base> without a remote), and the one "in dev" and ahead/behind compare with. |
name |
"{number}/{n}-{slug}": each branch in the issue's folder |
The branch's name. {number}: the issue's project number with its prefix, in lower case (vm-62), once the project numbers its issues (see Numbers), else its number on its first forge, else its id's first 7 characters. {n}: the branch's place in the issue's folder, one more than the highest among its branches and the base's last merge commits that name one: fix/2518/1-…, then fix/2518/2-…; counted, never stored. {kind}: its kind, through kinds; without one, {kind}/ is dropped. {slug}: its title in lower-case words, without a leading [BUG](Scope) or a trailing (#1234), cut at about 50 characters. A name without {n} ({kind}/{number}-{slug}, the form before folders) still links its branches. |
kinds |
none | The project's kinds, each to the {kind} of its branch: the kind itself, always (bug = "bug"; a branch starts with its kind's label, and bug = "fix" is refused with its line). git board lint reports a label description that names another prefix ("should prefix branch by fix/" on bug). They are the kind field (a column, a group, a filter, one per issue) in either of two forms. Without options in [fields.kind], the default, a kind is GitHub's plain label (bug), shown and filtered as kind:bug, in kinds order; [fields.kind.colors] and descriptions still apply. With options in [fields.kind], the kinds are kind:bug labels like any field's. git board project pull and sync fill a missing kind from the Project's Category (FEAT → feat); git board lint reports an issue with two kinds, a title starting with a bracket ([BUG]: the kind is its label, which wins when they disagree), and how many have none. |
[BUG](Equipment) Other equipment shows the wrong example (#2518), labeled bug, gives bug/2518/1-other-equipment-shows-the-wrong-example, and its second branch bug/2518/2-…. What git doesn't take in a branch name becomes -. The issue's head makes the branch from its plus (Create: git branch --no-track from the base, then git push -u origin, the server's checkout untouched, on a server listening on localhost only), or gives the commands to run yourself.
A branch names an issue when its first number is the issue's number, with or without the project's prefix (fix/vm-62-…, fix/62-…, 2518-…: its project number once the project numbers its issues, else a forge's), or when a word in it starts the issue's id (7 characters or more). Local branches and the remotes' (as last fetched) are listed.
Experimental features¶
These are the toggles of Settings › Experimental; this section is the reference.
Some features are still being built, and stay off unless the team turns them on. git-board's core, your issues kept in git and synced with whatever forge, works without them.
| Feature | What it turns on |
|---|---|
cycle |
The development cycle: the cycle on the issue page, its steps and gates, step 4's tests and checks, the Work page, Settings › Cycle, and git board verify. |
agents, worktrees |
Nothing, since v0.12: they were v0.11's coding agents and Settings › Worktrees. git-board knows the names, so a file that lists them gets no warning; the agents are git-board-agents now, which needs no switch (Worktrees). |
The file's list is the team's. One person trying a feature alone sets it in their clone's git config, a comma list that replaces the file's list in that clone (empty turns them all off there):
git config board.experimental cycle # this clone
git config --unset board.experimental # back to the file's list
While a feature is off:
- its commands are hidden from
git board --helpand refuse with a sentence naming the switch (git board verifyis the cycle's); - its API routes answer 404 with
{"code": "experimental", "error": "…"}, the error naming the switch: the cycle's,/api/issues/{id}/cycle…and/api/cycle(the HTTP API); - the web UI hides its pages, tabs, menu entries and controls:
GET /api/melists the features on asexperimental; - with the cycle off, a comment carries no step, and
[cycle]is not checked: what a broken table fails is a warning in Settings › The file, not an error that stops every command. With the cycle on, it is refused as before.
A name the list doesn't know is a warning, and ignored.
Cycle¶
On or off. The cycle is experimental: with "cycle" in [experimental] features, a board with a [cycle] table walks the cycle. Any of its keys, or a subtable such as [cycle.signoffs] alone, is enough, even an empty [cycle]. Turning the cycle off loses nothing: the cycle's comments, verdicts and sign-offs stay comments, and come back as the cycle's when it is on again.
With the cycle on, an issue's page shows its step in the development cycle (docs/prd/cycle-protocol.md) above the description, in one box: the project's steps, a cell each, numbered in order (by default the seven, Intent, Risk, Plan, Tests, Code, Review and Verify: steps, below), the current step with its gate and whose turn it is, the issue's branches, the pull request with its checks (Open, Edit, Merge, Check again), the environments, and the controls the server offers the viewer now: Accept without criteria, the description's verdict, the risk class, the status moves, Approve the plan, Mark ready for review, the review, and at Verify the criteria's checks, a pasted run and the sign-off (the web app). The step is never stored: the server computes it on every read from the status and the evidence. The status is the floor (its stage gives the first step it can be at), and the evidence moves the step within that stage, never below it. Evidence that disagrees with the status shows after the line ("plan not approved"), it never makes another step.
[fields]
risk = { options = ["low", "normal", "structural"], single = true }
[cycle]
ready = "ready"
in-progress = ["in-progress"]
in-review = ["in-review"]
validation = ["ready-for-validation", "qa", "tests-metier"]
criteria = ["Acceptance criteria", "Done when", "Expected"]
plans = "docs/src/implementation-plans"
plans-in-pr = true
risk = "pm"
plan = "lead-dev"
review = "reviewer"
[cycle.kinds]
bug = "failing-test"
feat = "per-criterion"
doc = "none"
chore = "none"
[cycle.signoffs]
ready-for-validation = "lead-dev"
qa = "pm"
tests-metier = "metier"
Every key has a default, so a project writes only what the guess got wrong, and Settings › Cycle says what was guessed. The stages are guessed from the status options, in their order, on the words of each name (split at -, _ and spaces). The last option is the closing status. A word among validation, qa, uat, acceptance, test, tests, metier, verify or staging makes a validation status; review, reviewing or pr makes in-review; progress, doing, wip, dev, development, implementing or coding makes in-progress; the first with ready, todo, planned or selected is ready. Every other status is before ready. With co2-calculator's nine statuses the guess is the table above. The roles are unset by default, and a gate whose role is unset, or names nobody, is anyone's turn.
| Key | Default | What it does |
|---|---|---|
steps |
["intent", "risk", "plan", "tests", "code", "review", "verify"] |
The project's steps, in order: the step bar's cells and the discussion's segments. A step's id is its kind (intent, risk, plan, tests, code, review), verify, or a sign-off (a validation status or a signoffs key). The built-ins keep the cycle's order, then come the sign-offs and verify in any order, so a review has the PR that code opened and a sign-off the merge: ["code", "review"] and ["intent", "risk", "code", "verify"] read, ["review", "code"] is refused when the file is read (git board lint, serve, the TUI, Settings' save) with the reason, as are an unknown id and one listed twice. A step left out holds nothing back: without intent no criteria, without risk no class, without plan no approval, without tests no test, as the rule none. Gates belong to the kind of step, so the list never changes what a step requires. An issue is at the last listed step it has reached; before the first, no cell is current. |
ready |
guessed | The status an issue with acceptance criteria waits at: steps 1 to 3 (intent, risk class, plan). |
in-progress |
guessed | Steps 4 and 5: the failing test, then the code and the PR. |
in-review |
guessed | Step 6: the review. |
validation |
guessed | Step 7, one sign-off per status, in the options' order. Signing off the last one moves the issue to the closing status and closes it. With none, Verify happens in in-review once the PR is merged, and its sign-off closes the issue. |
criteria |
"Acceptance criteria", "Done when", "Expected" |
The headings a description's acceptance criteria are listed under, any heading level or a bold line, in any case, with or without a colon. The first one the description has counts. Step 1 edits that list a row at a time and writes it as - [ ] checkboxes; a list with no heading above it is flagged. |
plans |
"docs/src/implementation-plans" |
Where plan files live: Markdown whose front matter's issue: names the issue (2518, "#2518", CO2-2518, or its id's first 7 characters), and whose status: (accepted, in-progress or delivered) approves the plan. |
plans-in-pr |
true |
The agent started at step 3 commits its plan and opens a draft PR holding it; Review opens once that PR is marked ready. false keeps plans out of PRs: the plan is committed on the branch and the PR opens at step 5. |
test-files |
*_test.go, *.test.ts, *.spec.ts, test_*.py, tests/** |
Which changed files are tests, by path or base name; dir/** is everything under dir. Before M4 this key was tests, which now names the suites (Tests and checks, below): a list there is refused. |
spec |
none | The role ([roles]) that gives the description's verdict: its Approve vN moves the status to ready. Unset, the author moves it, and anyone but the author may approve. |
risk |
none | The role ([roles]) that sets the risk class. |
plan |
none | The role that approves the plan. |
review |
none | The role that reviews the PR. |
signoffs |
none | Each validation status to the role that signs it off. With entries and no steps, verify leaves the default list and every validation status is a cell after Review, in the options' order, its role from signoffs when keyed and none otherwise; a key no status names comes last. Listed beside sign-offs, verify holds the validation statuses none of them has, so it stands where those statuses fall. |
signers |
".allowed_signers" |
The file, at the base branch, of the SSH keys whose signed results count (Tests and checks, below). |
kinds |
bug = "failing-test", feat = "per-criterion", doc = "none", chore = "none" |
Step 4's rule by the issue's kind (the kind: field, or a flat label in [branches] kinds): a test that fails before the fix; the suites the diff touches green and each criterion naming its test; or no tests, which skips step 4. A file's entries go over the defaults. No kind, or one the table doesn't list, is per-criterion. A kind also takes the table form, [cycle.kinds.bug] tests = "failing-test"; steps under a kind is a later milestone's, and until then gets the warning any key git-board doesn't know gets. |
git board lint says how it read the file, under read:, a line for each of the forms above it filled in (steps: none, so the default list: intent, risk, plan, tests, code, review, verify, [cycle.signoffs] qa = "pm": the sign-off step qa, at status qa, held by the role pm), so a project can write the explicit form when it wants to.
Each step's button moves the status, through the same edit as the status field:
| Button | Who | Moves the status to |
|---|---|---|
| Move to ready | the author, once the description lists a criterion | ready |
| Approve vN (the verdict bar) | the spec role, once the description lists a criterion; set, it replaces Move to ready |
ready |
| Approve the plan | the plan role |
the first in-progress |
| Start work | the assignee, at low risk or once the plan is approved | the first in-progress |
| Mark ready for review | the assignee, with a test, or at once when step 4 is skipped, once every check is proven at the branch's head (Tests and checks, below) | the first in-review |
| Start verification | the assignee, once the PR is merged | the first validation |
| Sign off | the status's role | the next validation, then the closing status |
A card dragged on the board still moves the status: the step follows it, with a note when the evidence disagrees.
The step reads:
- The status (the status field).
- The acceptance criteria: the list under the first of the criteria headings the description has (the format is below). With none, the issue is at step 1.
- The risk class: the risk field, low, normal or structural. Low skips step 3.
- The plan file, read from the evidence branch, then the plan/ branch, then the base.
- The tests: the files the evidence branch changed (git diff base...branch) that match test-files. The evidence branch is the newest branch naming the issue that isn't a plan/ branch.
- The PR, read from GitHub only when the issue is at in-review or later (or structural, for the plan branch's PR), at most every 5 minutes per branch. When GitHub can't be reached, the step still computes, with the last PR it read.
- The kind, for step 4's rule.
- The comments with metadata that the step writes. Each carries its step's id as cycle:step (intent, review, qa), never its number, as does a comment posted on a step's discussion; 1 to 7, written before the list, read as the default list's ids (6 is review). A comment without it, or with an id the list doesn't have, sits in the segment running when it was written.
The criteria in a description:
- The heading is one of criteria, as a Markdown heading of any level (## Expected) or a line in bold (**Done when:**), in any case, with or without a colon. The whole line is the name: ## Expected results isn't Expected. The first one in the description counts.
- The list starts at the first line under it that isn't blank. Each item is a line starting with -, *, +, 1. or 1); a checkbox (- [ ], - [x]) is dropped from its text. Blank lines between items are fine.
- The list ends at its first line that is neither blank nor an item: a paragraph, or the next heading.
Intent warns, naming the line, where the description reads differently from how it looks. The warnings never block a step: - a second criteria heading: it and its list are ignored; - a line that stops the list while items follow before the next heading: those items don't count; - a sub-item: counted as a criterion of its own; - a criteria heading with no list right under it.
The card writes:
- Set the risk: a field edit.
- Approve the plan: a plan:approve comment. A structural plan is approved on its PR on GitHub instead.
- Go on without criteria: an intent:criteria=none comment, for an issue from before git-board that has none. Step 1 counts as done and Verify asks for the sign-off alone; criteria written later take over. With [cycle] spec set, only that role's holder presses it; without it, anyone.
- Post the run, and tick a criterion: verify:run and verify:criterion comments, with the commit they were made at. A criterion's [ ] box in the description is ticked too: a tick, not a new version. An item with no box keeps the comment alone, since adding the box would be an edit.
- Sign off: a signoff:status comment, then the next status. A sign-off needs a run at the branch's head, or a reason, written as signoff:why.
A comment is the viewer's when git-board's identity wrote it, since the server writes as that identity. Anyone else's comment counts under its author's forge login. The server refuses a card action from anyone but the gate's person with waits for the <role> role: <login>.
Tests and checks¶
Step 4 runs the project's own commands, and keeps each result as proof in git: a signed line in a git note, which any clone can read and check without git-board's server or the forge (docs/adr/0001-check-results-are-signed-git-notes.md). Two tables, with the same keys:
[cycle.tests.<suite>]is a test suite. Step 4 lists the suites, ticks the ones the diff touches, and runs the ones you pick. Tests never gate a move.[cycle.checks.<check>]is a fast check: lint, vet, a type-check. Mark ready for review waits until every check is proven at the branch's last commit.
[cycle]
signers = ".allowed_signers" # the default
[cycle.tests.go]
run = ["go", "test", "./..."]
files = ["**/*.go", "go.mod", "go.sum"] # everything it reads
tools = [["go", "version"]]
[cycle.tests.web]
run = ["npm", "test"]
dir = "web"
files = ["web/**"]
[cycle.tests.e2e]
run = ["npx", "playwright", "test"]
where = "ci" # the forge's: listed, never run here
[cycle.checks.vet]
run = ["go", "vet", "./..."]
files = ["**/*.go", "go.mod", "go.sum"]
env = { CGO_ENABLED = "0" }
| Key | Default | What it does |
|---|---|---|
run |
required | The command, as an argv. It is never a shell line, and nothing from the page reaches it: the page only picks a suite by name. |
files |
the whole tree | Git glob pathspecs (**/*.go, web/**) of every file the command reads. They make the result's key, and they decide which suites the diff touches. List the lock files and generated inputs too. If the list is too narrow, a change the command reads keeps an old proof. |
dir |
the checkout's root | Where the command runs, relative to the root. It can't contain ... |
where |
"worktree" |
"ci" marks a suite that the forge's CI runs (e2e, Lighthouse, performance). It is listed but never run here, and a check with it doesn't gate. |
env |
none | Variables the command runs with. Names are in capitals, and they are part of the key. The only others passed through are PATH, HOME, TMPDIR, GOCACHE, GOMODCACHE, GOPATH, GOFLAGS and npm_config_cache. |
tools |
none | Version commands, such as [["go", "version"]]. They run in the checkout before the command, and the hash of their output goes into the line as tools=, so a reader can see two runs used the same toolchain. |
A suite's name is letters, digits, _, . and -. The table [cycle.tests.<suite>] used to be the key tests under [cycle], the test-file patterns, which is now test-files. A list under tests is refused.
The base branch's file rules. Step 4 and the gate read .git-board.toml and the signers file from the base branch. The base is origin/main, or whatever Branches works out, never the issue's branch. This means a branch can't add a command that then runs on its reviewer's machine, and it can't add its own signer. A change to these tables counts once it is committed on the base. Settings › Advanced still shows the working tree's file.
A run. Run the selected answers at once, and the run goes on in the background. A server runs one run at a time; a second one gets busy. Each picked name runs like this:
git worktree add --detachchecks out the commit into a temporary directory, so nothing uncommitted leaks in.- The
toolsrun, thenrunruns fromdir, with the environment cleared, for 20 minutes at most. - Exit 0 is PASSED. Any other exit, or the timeout, is FAILED. A command that can't start at all, such as a missing binary, records nothing and says why: neither the agent nor a missing tool decides a verdict.
- When the run ends, a
cyclelive event reloads the page.
Run the selected runs at the head. For a bug, a second button, Run at
The line. A result is one line, appended to a note:
v1 vet PASSED key=5f3a… platform=darwin/arm64 tools=sha256:9c1e… commit=4c1e2a0… at=2026-10-22T09:14:03Z dur=12s log=sha256:e3b0… sig=U1NIU0lH…
The note isn't attached to the commit but to a content key: the hash (git hash-object) of a text that names the check, its run, dir and env, and the blob of every file files selects at the commit. So a commit that changes nothing the check reads keeps its proof, and so does a rebase. commit= is there for reading only.
Signing. git's user.signingkey names the SSH key file, the same one gpg.format = ssh signs commits with. A .pub path works too: ssh-keygen then finds the private key beside it, or in ssh-agent. A key written into the config itself (key::…) is refused, because git-board signs with a key file. The line is signed with ssh-keygen -Y sign -n git-board-ci. If user.signingkey is unset, Run says so before anything runs.
The signers file. The file is [cycle] signers, .allowed_signers by default, in OpenSSH's allowed_signers format, with one line per person:
ssh-keygen -y -f ~/.ssh/id_ed25519 prints the key part. A line counts only when its signature verifies against this file, as read at the base branch. A signature made with another namespace (git's own commit signatures use git), a changed line, or a key the file doesn't list is dropped. Without the file nothing verifies, so nothing is proven.
The writer. Each clone writes to its own ref, refs/notes/ci/<writer>. The writer name is git config git-board.writer, set on the first run to <user.name>-<host>-<4 hex>, so two clones of one person never share a ref; worktrees of one clone share it. A write is a compare-and-swap (git update-ref <ref> <new> <old>, retried), because git notes append alone loses lines when two worktrees write at once. A reader takes the union of refs/notes/ci/* and, for fetched results, refs/notes/remotes/*/ci/*. git-board doesn't push or fetch the notes yet. By hand:
git push origin "refs/notes/ci/$(git config git-board.writer)"
git fetch origin 'refs/notes/ci/*:refs/notes/remotes/origin/ci/*'
Never run git notes prune on these refs: no object stands behind a content key, so prune would delete every result. And don't set notes.rewriteRef for them: a rebase must not copy a proof onto content it didn't prove (the key already carries a proof across a rebase that leaves the files alone).
The verdict. For each writer, its last line that names the check and the key, and that verifies, is that writer's verdict. A check is proven when one writer's verdict is PASSED and none is FAILED, so a red run stays red until that writer runs the check green again.
The gate. Mark ready for review is a POST /api/issues/{id}/cycle/ready-for-review. It moves the status only when every [cycle.checks.*] that doesn't have where = "ci" is proven at the branch's head. Otherwise it answers 409 unproven with the reason, which shows under the card. The reasons are:
vet FAILED, signed by alice on 2026-10-22 11:14:03;vet not run at 4c1e2a0;no signers file .allowed_signers on origin/main: no result can verify.
With no checks declared, the gate is open, and so it is for an issue with no branch. Tests never gate. The gate belongs to the card: dragging the card on the board, or editing the status field, still moves the status.
The rules. For a bug (failing-test), step 4 shows two runs of the touched suites:
- Before the fix, at the commit that added the first test file. It should be red: Red first: done. A suite that passes there "doesn't show the bug".
- At the head, which should be green, after the fix in step 5.
For a feature (per-criterion), the rule is the touched suites green at the head, with each criterion naming its test. The page shows both rules, and only the checks gate.
The log. A run's output is kept in the clone that ran it, at <git common dir>/git-board/ci/<sha256>; the line carries its hash. Click a result to read it. A run from another clone has no log here, and the page says so.
Proposing checks. With no [cycle.checks] on the base, step 4 offers Propose from the project. It reads the first source it finds:
- lefthook's
pre-commitcommands, fromlefthook.yml,.lefthook.ymlorlefthook.yaml. A command'sroot:becomesdir, withfiles = ["<root>/**"]. Other hooks are left out, since the gate does their job. - The Makefile's targets
lint,vet,typecheckortype-check, asmake <target>. - The scripts
lint,vet,typecheckortype-checkinpackage.json, or in a*/package.jsonone directory down, asnpm run <script>from that directory.
Only a command that runs the same in a clean checkout is proposed. Each of these is skipped, with the reason:
- a lefthook command that works on
{staged_files}or another placeholder; - a shell line (
$,|,&&, quotes, a glob); - a
sh,bashorzshscript.
Write to .git-board.toml adds the [cycle.checks.<name>] tables to the working tree's file, through GET and POST /api/checks/setup. Trim files there, then commit the file on the base for the checks to gate.
Only from this machine. A test run executes the branch's code and signs its proof with your key, so POST /api/issues/{id}/tests/run and POST /api/checks/setup answer 403, as making a branch does, unless the server listens on localhost alone (exposed otherwise) and the request comes from the board's own page (origin otherwise).
Verifying without the server. git board verify [commit] applies the same gate in any clone, with no server. The commit defaults to HEAD. It prints each check's verdict at the commit, and exits 1 when the commit isn't proven. The base defaults as on the server: [branches] base, else origin's default branch, else main, and origin/<name> when that exists. --base names another.
$ git board verify fix/2518-retry
4c1e2a0 against the checks on origin/main
typecheck not run
vet PASSED, signed by alice on 2026-10-22 11:14:03
error: not proven: typecheck not run at 4c1e2a0
The Work page¶
Work (/work, first in the header) is what needs a person, computed from every open issue's step. Mine is the viewer's: Needs you lists the issues whose gate waits for the viewer, or where another person asked them something, oldest wait first, with the step, what it waits for, since when, and the action (the three risk buttons, Answer, Read the plan, Review, Sign off, or Open); Waiting on others lists the issues the viewer is involved in (author, or named by a field such as pm:alice, on the issue or inherited from [roles]) whose gate waits for someone else. Team is every open issue: anyone's gates join Needs you, and the rest join Waiting on others. An issue is in one group at most. What waits for the forges is Settings › Sync's.
A question is a comment by someone else than the viewer with ask metadata, or, failing that, whose text ends in a question mark; the viewer's next comment answers it. Since is the issue's last state change (a status, label, title or card comment), not a plain comment. GET /api/cycle?who=me|team returns the groups, paged as the lists are; it needs an identity with a login, as the Inbox does.
Worktrees¶
git-board starts no coding agent and makes no worktree: v0.12 moved them
to a program of their own, git-board-agents (git board-agents, in
contrib/board-agents until it has its own repository; ADR
0021). It runs
wtx, one agent, one worktree, one
branch, one PR, and speaks to the board through git-board's command line
and HTTP API alone. Its README says how to install it, what it needs, and
its commands: start, brief, approve, status, stop, done,
risk.
Settings › Worktrees, the issue page's agent panel and the Work page's Agents were v0.11's; they are not in git-board's web app.
Numbers¶
A project can number its issues itself, CO2-3075, instead of leaning on a forge's numbers. The counter is in git, refs/board/seq on origin (never a mirror): no key here. A board kept elsewhere (Where the board is kept) keeps it there: a row in PostgreSQL, <folder>/.board/numbers in a Markdown folder, which numbers on the default branch alone (on another, Settings › Numbers says why numbering waits). Start it once, for everyone, from Settings › Numbers (#/settings/numbers): it suggests a prefix from the repository's name (git-board → GB, co2-calculator → CO2), takes the start (above every number an issue has on a forge, below), and names what it pushes and where before Start numbering. Once started, Number them now numbers the issues origin has without one; git board numbers also says how many wait. Or from a terminal:
Choose the prefix once: issues carry it wherever a forge could read the number, in branches (fix/co2-3075-…), pull request titles, commits and messages. A bare number collides with the forge's own: GitHub links #3075 or a branch's 3075 to its issue 3075. start is one more than GitHub's highest issue or pull request number (gh api 'repos/OWNER/REPO/issues?state=all&per_page=1' -q '.[0].number'). GitHub numbers below it stay the issues' own: #2518 is CO2-2518, even for an issue imported later. Every other issue, made here or brought in from a forge, gets the next number with the next push to origin you ask for: Sync in Settings › Sync or git board sync, Number them now in Settings › Numbers, or git board numbers. The counter only names issues origin has, so other clones never see a number for an issue they can't get: git board numbers numbers what was pushed already, and a sync pushes the issues first. Until then an issue shows its short id. Nothing pushes the counter on its own: not creating an issue, not a forge sync. Pushing the counter is the lock: when two clones number at once, git refuses the second push and that clone takes the next number.
After the switch, a bare number (3075, fix/3075-…) and CO2-3075 are the project's number; #3075 is always a forge's. git-board always writes a project number with its prefix; #N is only ever a forge's. Cards, rows and the issue page show the number before the title.
Release¶
These are the forms of Settings › Release; this section is the reference.
[release] is how the project's code reaches its environments: the ordered environments, the checks a merge must pass, what to build (images, chart) and the config repos holding each environment's overlay ([[release.deploy]]). Without environments or binaries, the project has no releases. See the release design.
A Go program with no deploy targets lists binaries instead: the main packages to build per platform and publish as archives on the forge's release. The Releases page writes these for you: Enable binary release infers them from the repo.
| key | meaning |
|---|---|
binaries |
main packages, ["./cmd/git-board"]. A table with binaries and no environments is enabled: every cut is a final, deployed nowhere. |
platforms |
os/arch to build for; default linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64. |
build |
a command run with sh -c in a clean checkout of the tag instead of the Go builds, "make dist"; it leaves the archives in dist/. |
The environments have roles, from their order. The one that follows the trunk takes its head (dev when listed, else none). The last takes final releases. Those between take candidates. One environment that is the followed one has no releases.
[release]
remote = "origin"
environments = ["dev", "stage", "prod"]
checks = "make ci"
images = [
{ name = "frontend", context = "./frontend" },
{ name = "backend", context = "./backend" },
]
chart = { path = "./helm", registry = "oci://ghcr.io/acme/app/helm" }
version_values = ["backend.env.APP_VERSION"]
[[release.deploy]]
repo = "git@github.com:acme/k8s-app-config.git"
path = "apps/app/overlays/{env}"
registry = "ghcr.io/acme/app"
[[release.deploy]]
repo = "git@github.com:acme/openshift-app-config.git"
path = "apps/app/overlays/{env}"
registry = "quay.example.org/acme"
| Key | Default | What it does |
|---|---|---|
remote |
origin |
The authority: tags and release records are pushed there, and a refused push is the lock. Other remotes are mirrors. |
environments |
none | The environments in order, each once. The last takes finals, the ones between follow and the last take candidates. |
follow |
"dev" when listed |
The environment that follows the trunk's head. It must be one of environments; follow = "" means none does. |
checks |
none | The command run on a merge's exact tree before it reaches the trunk (make ci). |
images |
none | The images to build, each a name and a docker build context. |
chart |
none | The helm chart: its path and the OCI registry it is pushed to. |
version_values |
none | Dotted paths under the chart entry's valuesInline that get the version as a string (backend.env.APP_VERSION). |
builder |
the local buildx builder | A remote buildx builder to build on. |
deploy |
none | One [[release.deploy]] per config repo: its repo, the overlay path (with {env}, replaced by the environment) and the registry of its images. |
A deploy edits the overlay's kustomization.yaml and nothing else (comments, key order and other entries stay): the images[] entry named <registry>/<image> gets newTag and digest; the helmCharts[] entry whose repo is the chart's registry gets version, and each version_values path under its valuesInline gets the version; commonAnnotations git-board/commit and git-board/version record what runs.
Repos¶
These are the forms of Settings › Repos; this section is the reference.
list is the GitHub repositories the project spans, the first one numbering the issues; without [repos], origin's when it is on GitHub.
git board lint --create-labels compares the labels the file implies with each repository's, and each [forges] table's, and --apply makes the missing ones there with their colors and descriptions (lint). Settings › Labels doesn't compare them.
An issue's assignee:<login> labels are always the forge's own assignees, on every forge (GitLab's free edition takes one; the others stay in git-board).