# Release checklists
What to look at before a version is committed or shipped, one box per item -- or one per item and per member when
a team tests it together -- saved as it is checked. A checklist may follow a `` board: its items then
name the tickets they verify, a ticket all of whose items are checked is labelled verified on the board, and can be
moved to done from the checklist, which comments in its discussion what was checked.
| | |
|---|---|
| `MMO\admin\helpers\Checklist` | a checklist: its items, its checks, its progress, its tickets' progress |
| `MMO\admin\helpers\ChecklistBuilder` | builds the items from a recipe: `git status` of repositories, the board's tickets, items written by hand |
| `MMO\admin\helpers\KanbanBoard` | a board's file: tickets, discussions, labels, moves, and its lock |
| `MMO\admin\helpers\BoardFileRepository` | a board's file as mmo.client's kanban entities (mmo.git's `BoardSync` with GitHub) |
| `MMO\admin\controllers\ChecklistController` | the checklists' JSON API |
| `MMO\admin\controllers\BoardController` | the boards' JSON API: a board, its discussions, its tickets' AI prompts |
| `{MMO\admin}/templates/checklists.phtml` | the page of the checklists: one (`?name=`), the default one, or every one (`?list=1`), a selector between them |
| `{MMO\admin}/templates/board.phtml` | the page of a board (`?board=`, `?ticket=12` opens a ticket) |
| `{MMO\admin}/pages/checklists`, `pages/boards` | the two templates as pages, in English |
The `mmo-release-bugs` agent skill (`.claude/skills/`, also a prompt of the MCP server) is the whole method around
it: a bug report becomes tickets with a fix prompt each, agents fix them and report in the tickets, the checklist
proves them.
## Adding them to an application
```jsonc
// configs/config.json
"MMO\\core\\Application": {
"routes": {
"MMO\\admin\\controllers\\ChecklistController": {},
"MMO\\admin\\controllers\\BoardController": {},
"MMO\\client\\controllers\\PageController": {} // after them: its `**` route catches everything
}
},
"MMO\\admin\\controllers\\ChecklistController": {
"folder": "{root}/checklists", // one JSON file per checklist
"boards": "{root}/boards", // the boards a checklist may follow: {boards}/{board}.json; null for none
"scope": "admin", // mmo.oauth: who may read and check; null on a local tool
"anonymous": false, // true: the routes answer whoever calls, logged in or not
"checklists": { // the checklists the application declares (see below)
"general": {"title": "Release checklist", "recipe": {"itemsFile": "{root}/src/checklists/general.json"}}
},
"default": "general", // the one the page opens when it is given none
"verifiedLabel": {"id": "verified", "label": "verified", "color": "green", "icon": "check"},
"doneComment": "**Release checklist**: verified, moved to **done**."
},
"MMO\\admin\\controllers\\BoardController": {
"folder": "{root}/boards", // one JSON file per board; null: no boards (every route 404)
"scope": "admin",
"anonymous": false
},
"MMO\\client\\Theme": {"pages": ["{root}/src/pages", "{MMO\\admin}/pages"]}
```
Rather than mmo.admin's pages (in English, a menu group of their own), a page of the application imports the
templates, in its words (every key optional; `texts` lists each template's words at its top):
```php
= $this->import(\MMO\filename('{MMO\admin}/templates/checklists.phtml'), ['locale' => 'fr-FR', 'texts' => ['title' => 'Checklists', ...]], false) ?>
= $this->import(\MMO\filename('{MMO\admin}/templates/board.phtml'), ['board' => 'acme', 'checklists' => \MMO\url('admin/checklists')], false) ?>
```
The web server writes the files: the folders must be writable by it. Without `scope`, whoever reaches the routes
writes the checklists and the boards: keep that to localhost, as `anonymous` (the agents' `curl` on a development
machine, `configs/dev.json`). my.oviepro has both: its general checklist in production (`{db}/checklists`, `sudo`),
the board and the review checklists in dev only.
### Declared checklists
A checklist of `checklists` is made the first time it is asked for, and kept up to its declaration (`title`,
`description`, `members`, `types`, `groups`, `board`, `boardUrl`, `recipe`). Its items are built from its recipe when it
is made, and again whenever what they come from changes -- the recipe, or its `itemsFile` (a new version of the
application asking other checks) -- for a recipe of `items` and `itemsFile` alone; a recipe reading repositories or a
board is built by `build.json` only. What was checked stays checked while its item is there. Such a checklist is the
general one of an application: what is tested before each version, round after round (`reset.json`).
## The file
`{folder}/{name}.json`:
```jsonc
{
"version": 4, "updatedAt": "2026-10-05T14:17:00+02:00",
"title": "my.oviepro 4.1: review before commit", "description": "...",
"board": "my.oviepro-deploy", // optional: the board of the tickets
"boardUrl": "boards/oviepro-deploy", // optional: its page, where `?ticket=12` opens a ticket
"members": [], // optional: one box per member
"recipe": {...}, // optional: how `build.json` makes the items
"types": {"pdf": {"label": "PDF", "icon": "file-pdf"}}, // optional: what each `kind` is called, and its icon
"groups": {"General": {"icon": "globe"}}, // optional: a group's icon
"items": [{"id": "3d332c914651", "group": "mmo.core", "section": "src/entities", "kind": "code",
"title": "src/entities/Entity.php (modified)", "detail": "Review the diff: `git -C ... diff -- ...`",
"url": null, "note": "", "tickets": ["80"]}],
"checks": {"3d332c914651": {"*": {"at": "2026-10-05T13:34:55+02:00", "by": "marc"}}},
"release": {"name": "4.1", "startedAt": "...", "startedBy": "marc"}, // the round being checked
"history": [{"name": "4.0", "startedAt": "...", "startedBy": "marc", "closedAt": "...", "closedBy": "marc", "done": 700, "total": 760}]
}
```
- An item's `id` is `sha1(group|section|title)`, 12 characters: rebuilt items keep their checks, a reworded item
starts again unchecked.
- `group` is what the page folds (a repository, the pages, "before committing"), `section` a heading in it (a
folder, a page), `kind` what it is (`code`, `page`, `global`, a test's type), `url` where to look, `detail` how
(`` `code` `` shows as code, nothing else is markup).
- `checks[item][member]`, `*` without members. An item is done once every box is checked.
- `note` is a word on the item's section (a page's), shown by its heading; a section whose items share an `url` links it.
- The first check starts the round (`release`); `reset.json` closes it -- every box unchecked, a line of `history`
saying how far it got -- and names the next.
## The recipe
```jsonc
{
"repositories": [
{"label": "acme", "path": "~/dev/src/acme", "together": ["modules/old/"], "exclude": ["libs/"], "pages": true}
],
"treated": "/\\*\\*Claude — (résultat|result)\\*\\*/u",
"site": "http://localhost/acme/",
"pages": "acme — pages",
"checks": {"12": {"page": "gestion/accidents", "check": "sorted by date, newest first", "url": "gestion/accidents"}},
"items": [{"group": "before committing", "section": "checks", "title": "`php tools/mmo.php verify` passes"}],
"itemsFile": "src/pages/admin/release-checklist.items.json",
"texts": {"diff": "Relire le diff : `git -C {root} diff -- {path}`"}
}
```
1. **items**, **itemsFile**: written by hand, first. The file holds flat items, or a page by page test:
`{members, types, sections: [{label, icon, pages: [{label, path, note, checks: [{label, type}]}]}]}`, whose `members`,
`types` and sections' icons the checklist takes too (the section is the group, the page the
section, the check's type the kind).
2. **repositories**: one `code` item per file of `git status --porcelain -uall`, under the repository's `label`; its
section is its folder (two levels, three under `src/`), a page's folder for a file of `src/pages` unless
`"pages": false`. `together` makes one item of a folder's files, `exclude` leaves paths out. `~/` is the home of
whoever owns the application's folder (not the web server's). The item names the treated tickets whose
discussion cites the file -- by its path, or its last three or two parts, `a/{b,c}.php` written out; a file no
ticket cites says it is a transverse change.
3. **pages**: for each treated ticket -- one whose discussion matches `treated`, or that `checks` names -- its
functional checks: `checks[id]` (one, or a list), else the last ```` ```check ```` block of its discussion, else
of its description, one check per line:
````markdown
```check
gestion/accidents | sorted by date, newest first
home (mobile 390 px) | home | the drawer closes on Escape
```
````
`page | what to check`, or `page | url | what to check`; a relative url is relative to `site`. A treated ticket
with no check gets one item asking to check its result on screen.
4. **texts**: the words of the generated items, in the application's language: `M`, `D`, `??`, `A`, `R`, `T` (the
states), `files`, `diff`, `new`, `deleted`, `orphan`, `pages`, `root`, `page`, `unchecked`, `uncheckedDetail`.
`POST api/checklists/{name}/build.json` answers the checklist and `build: {orphans, unmatched}`: the changed files no
ticket cites, the treated tickets no file names.
## The API
| | |
|---|---|
| `GET api/checklists.json` | `default`, and every checklist: name, title, description, board, version, release, progress, declared |
| `GET api/checklists/{name}.json` | one, with `progress` and `tickets` (each ticket its items name: title, status, total, checked, verified) |
| `PUT api/checklists/{name}.json` | `{version, title, description, board, boardUrl, members, recipe, items}`: create (version 0) or change; 409 when saved meanwhile |
| `POST api/checklists/{name}/build.json` | rebuild the items from the recipe |
| `PUT api/checklists/{name}/items/{item}.json` | `{checked, member}` |
| `POST api/checklists/{name}/reset.json` | `{name}`: a new round, every box unchecked, a line of `history` |
| `POST api/checklists/{name}/tickets/{ticket}/done.json` | move a verified ticket to the board's first done status, with a comment |
| Boards | |
|---|---|
| `GET api/boards.json` | every board: name, title, version, updatedAt, tickets |
| `GET api/boards/{board}.json` | `{board, title, version, updatedAt, data}`, each ticket counting its `comments`; an unknown board is empty, version 0 |
| `PUT api/boards/{board}.json` | `{version, data}`: the whole board; 409 with the board as it is when it was saved meanwhile |
| `GET api/boards/{board}/prompts.json` | `?agent=claude&status=todo&done=1`: the tickets' AI prompts, and `markdown`, the brief |
| `GET`, `POST api/boards/{board}/{ticket}/comments.json` | a ticket's discussion; `{body, parent}` comments it |
| `PUT`, `DELETE api/boards/{board}/{ticket}/comments/{comment}.json` | `{body}` changes a comment |
## From PHP
```php
use MMO\admin\helpers\{Checklist, ChecklistBuilder, KanbanBoard};
$board = KanbanBoard::Edit(\MMO\filename('{root}/boards/acme.json'), fn(KanbanBoard $b) => $b);
Checklist::Edit(\MMO\filename('{root}/checklists/acme-2.0.json'), function (Checklist $list) use ($board) {
$builder = new ChecklistBuilder($list->data['recipe'] ?? [], $board);
$list->setItems($builder->build());
print_r($builder->orphans());
}, true);
```