Generate meeting notes from GitHub or GitLab + Indico. This tool generates LaTeX code that can be compiled into a PDF presentation. The result looks something like this:
Issue and stale-item slides use the same compact layout as PR slides:
pip install mtng$ mtng --help
Usage: mtng [OPTIONS] COMMAND [ARGS]...
Meeting generation script, version 0.4.1
Options:
--install-completion [bash|zsh|fish|powershell|pwsh]
Install completion for the specified shell.
--show-completion [bash|zsh|fish|powershell|pwsh]
Show completion for the specified shell, to
copy it or customize the installation.
--help Show this message and exit.
Commands:
auth Authentication helpers
generate Generate a LaTeX fragment that includes an overview of PRs,...
preamble Print a preamble suitable to render fancy output
schema Print the configuration schema
$ mtng generate --help
Usage: mtng generate [OPTIONS] CONFIG
Generate a LaTeX fragment that includes an overview of PRs, Issues and
optionally an Indico agenda
Arguments:
CONFIG [required]
Options:
--token TEXT Github API token. Falls back to GH_TOKEN or
a token stored via `mtng auth login`.
--since TEXT Start window for queries. Required unless
--release is used. Accepts ISO and human-
readable values like "1 week ago".
--now TEXT End window for queries. Accepts ISO and
human-readable values like "now" or "next
monday". [default: now]
--event TEXT Optionally attach an Indico based agenda
overview. This only works with public
events!
--release TEXT Summarize merged PRs from a GitHub release
tag or release URL instead of a date window.
--preamble FILE Prepend this LaTeX preamble file to fragment
output. Mutually exclusive with --full.
--full Write a full LaTeX file that is compileable
on it's own
--pdf FILE Compile the report as a PDF file. This
requires a LaTeX installation.
--help Show this message and exit.
mtng consumes a configuration file to specify repositories and report sections. YAML (.yml / .yaml), TOML (.toml) and JSON (.json) are all supported.
TOML is the recommended format for new configurations. Define repository aliases
once, then list sections in report order. YAML and JSON accept the same schema.
See examples/report.toml for a complete example.
title = "Development update"
[repositories.acts]
name = "acts-project/acts"
display_name = "ACTS"
[[sections]]
type = "changes"
repositories = ["acts"]
state = "merged"
group_by = "category"
[sections.labels]
exclude = ["backport"]
[[sections]]
type = "changes"
repositories = ["acts"]
state = "open"
drafts = "exclude"
[[sections]]
type = "indico"
event = "https://indico.example/event/123/"
title = "Meeting agenda"Run with mtng generate report.toml --since "1 week ago" --tex report.tex.
Replace the example event URL with a public event, or omit the Indico section.
| Section type | Content and options |
|---|---|
changes |
PRs/MRs; state = "merged" (default) or "open". group_by = "category" or "none" (default). Merged sections include existing statistics and breaking-change slides; summary = false hides the statistics. |
statistics |
Existing statistics slide for merged PRs/MRs, without the item list. |
issues |
Open issues created during the reporting window. |
stale |
Open issues/PRs/MRs with the required label; existing new/all-stale slides. |
discussion |
Open issues with the required label, using existing discussion slides. |
indico |
Contributions from a public event URL, placed here in the report. |
Repository sections require repositories = ["alias", ...]; each repository is
rendered separately in the listed order. Optional title overrides their frame
titles using existing placeholders. Indico title is literal text. Repository
aliases refer to the existing repository settings (name, forge, host,
display_name, category styling, assignee/review display, etc.). Only referenced
repositories require credentials.
Section-local [sections.labels] supports include (all labels must match) and
exclude (any matching label excludes the item). These replace repository-level
label filters for that section. Section types control which data is collected;
legacy do_* content switches and stale/discussion labels on a repository do not
create additional sections. group_by controls category grouping locally.
Repository sections also accept a nested where predicate. For example, include
items with the tracking label or changes touching tracking files:
[sections.where]
any = [
{ labels_any = ["tracking"] },
{ paths_any = ["src/tracking/**", "tests/tracking/**"] },
]Each node has exactly one operator: all (AND), any (OR), not (negation),
labels_all, labels_any, or paths_any. Lists must be nonempty. Boolean nodes
can nest, for example:
[sections.where]
all = [
{ any = [
{ labels_any = ["tracking"] },
{ paths_any = ["src/tracking/**"] },
] },
{ not = { labels_any = ["backport"] } },
]Existing label, draft, date, and release selection remains unchanged and combines
with where using AND. To express label OR path, put the label inside where,
not in sections.labels.include. Omitting where preserves existing behavior.
Labels match exactly and case-sensitively. Paths are case-sensitive,
repository-relative POSIX globs: *, ?, and character classes match within one
path segment; a whole ** segment matches zero or more segments. Both old and
new paths of renames count, as do added and deleted files. paths_any is false
for issues (so its negation is true). Predicates are evaluated in written order
and short-circuit; changed paths are fetched only when needed. Detected truncated
file lists fail rather than silently producing incomplete selections.
Filtering precedes statistics, category grouping, and review follow-up grouping. Saved reports record fetched paths for offline replay. Old snapshots still work with existing configs; a new predicate that needs unsaved paths requires a new snapshot. Label-only predicate changes can use the items already in a snapshot.
For open changes, drafts = "include", "exclude" (effective default), or "only"
uses both native draft state and the repository's optional wip_label. Legacy
show_wip does not override this explicit selection. stale and discussion
sections accept the same drafts option, defaulting to "include"; issues are
never drafts, so "only" keeps just draft PRs there.
Merged changes and statistics can set release = "v1.2.3" locally. CLI --release
is their fallback; it does not suppress other explicit sections. --since is
required for period-based merged/statistics sections and for issues/stale sections.
It is optional for open-only, discussion-only, Indico-only, and release-only reports.
Use explicit Indico sections instead of --event in this configuration format.
Section ordering changes composition, not slide styling. Statistics, grouping, breaking-change treatment, item layout, and LaTeX/PDF output otherwise retain existing behavior. Contributors and revised review reporting are not introduced by this configuration refactor.
--save-data and render --config support explicit sections, including saved
Indico events. Sections can be reordered, restyled, or omitted offline. A new query
or event missing from the snapshot fails rather than accessing the network.
Pass --config (or -c) multiple times to share repository definitions between
meetings:
# repositories.toml
[repositories.acts]
name = "acts-project/acts"
display_name = "ACTS"# meeting.toml
title = "Weekly development update"
[[sections]]
type = "changes"
state = "open"
repositories = ["acts"]mtng generate --config repositories.toml --config meeting.toml --full --tex report.tex
# The existing positional config is also supported and is applied first:
mtng generate repositories.toml -c meeting.toml --full --tex report.tex
mtng render report-data.json -c repositories.toml -c meeting.toml --tex report.texFiles may mix TOML, YAML, and JSON. Mappings merge recursively; later values
win. Lists (including sections, legacy repos, and label filters) are replaced,
not concatenated. An empty list clears an inherited list; an explicit YAML/JSON
null replaces the earlier value where the schema permits it. For example,
[repositories.acts] in a later TOML file can override only display_name, keeping
the shared name and other settings. Omitted values remain inherited.
Validation runs after all files have been merged, so a shared repository file
needs no sections of its own. Only repositories referenced by the final sections
are collected. Saved report data contains the merged configuration. On render,
the merged config files replace the saved config, just as a single --config did;
the existing restrictions against requesting uncollected data still apply.
Before any of these files are read, mtng generate and mtng render load a
per-user configuration file as the lowest layer: config.toml inside mtng's
user configuration directory (~/Library/Application Support/mtng/config.toml
on macOS, ~/.config/mtng/config.toml on Linux). Machine-level defaults such
as the LLM gateway transport belong there, keeping meeting specs free of
host-specific settings; the project config overrides the user layer key by
key, and an invalid user config is reported like any other config error. The
GitHub Action ignores this layer — shared runners must not pick up a
maintainer's personal configuration.
Existing repos configurations remain supported without migration, in TOML,
YAML, and JSON. They retain their default sections, ordering, CLI --release /
--event behavior, and output. Do not mix repos with explicit sections.
To migrate, move repository entries to [repositories.<alias>], add the desired
[[sections]], and move selection filters into those sections. Presentation
settings can stay on the repository. The GitHub Action accepts section configs via
its config input; its existing inline inputs continue to produce legacy configs.
An example YAML configuration could look like this:
title: Weekly PR and issue update
footline_left: Core team
repos:
- name: acts-project/acts
stale_label: Stale
wip_label: ":construction: WIP"
show_wip: true
group_prs_by_category: true
pr_category_labels:
feat: New features
fix: Bug fixes
pr_category_colors:
feat: LimeGreen
fix: Purple
breaking: alertred
pr_category_order: [fix, feat]
do_recent_issues: true
no_assignee_attention: true
filter_labels:
- backportThe CLI supports GitLab.com and self-hosted GitLab, including CERN. Existing
configurations default to GitHub. Keep name as the project path; select GitLab
with forge and optionally host (an HTTPS hostname, without an API path).
Nested GitLab groups are supported:
[[repos]]
name = "acts-project/acts"
display_name = "ACTS on GitHub"
[[repos]]
name = "group/subgroup/project"
forge = "gitlab"
host = "gitlab.cern.ch"
display_name = "CERN project"
do_recent_issues = true
do_reviewers = trueUse unique display_name values if project paths repeat across hosts. GitHub
Enterprise and GitLab installations under URL subpaths are not supported.
The bundled GitHub Action remains GitHub-only; use the CLI for GitLab reports.
For a classic personal access token, select the read_api scope.
read_repository alone does not cover the MR and issue APIs; full api or write
access is not needed.
For a fine-grained personal access token, grant the following permissions.
Restrict project access to the repositories in your report (for example,
atlas/athena on gitlab.cern.ch):
| Access boundary | Permission | Level | Needed for |
|---|---|---|---|
| Project | Merge Request | Read | MR lists, approvals, diffs, and changed-path predicates. |
| User | User | Read | Token validation by mtng auth login and mtng auth check through GET /user. |
| Project | Work Item | Read | Issue collection, including issue, stale, and discussion sections. Omit for MR-only reports. |
| Project | Release | Read | Reports selected by --release or a section's release setting. Omit for date-based reports. |
The token's account must also have access to the configured projects. See GitLab's fine-grained REST API permission reference for the endpoint-to-permission mapping. These permissions provide read access; mtng does not require write permissions.
Store and validate either token type with:
mtng auth login --host gitlab.cern.ch
mtng auth check --host gitlab.cern.ch
mtng auth status --host gitlab.cern.ch
mtng generate report.toml --since "1 week ago" --full --tex report.texauth defaults to github.com; any other host selects GitLab. Tokens resolve from
--token, then GH_TOKEN (GitHub) or GITLAB_TOKEN (GitLab), then the system
keychain. --token requires exactly one backend/host in the configuration.
GITLAB_TOKEN is accepted only when there is one GitLab host. For several GitLab
hosts, unset that variable and run auth login --host ... for each host. Existing
GitHub keychain entries continue to work. Cached API results are separated by
host and credentials.
GitLab reports use MR terminology and !123 references. Existing do_*_prs
and pr_category_* settings apply to MRs as well; explicit frame-title overrides
are preserved. GitLab date windows include MRs whose merged_at lies within the
specified bounds, even if they were edited later. GitHub retains its existing
calendar-date search behavior.
For a GitLab release, use --release v1.0 or a matching project release URL.
The release description must reference its merged MRs using !123 or full
project MR URLs. References to other projects are ignored. A tag applies to
every configured repository; a full URL must match the repository and host.
GitLab review summaries include approvals and assigned reviewers. Comment and change-request histories are not inferred from account state. If an instance lacks the approvals endpoint, the report marks review data unavailable; authentication and server failures remain errors. Line/commit counts are left unknown rather than reported as zero. Detailed diff statistics and report-mode redesign are follow-up work.
Repository(object): Cannot contain additional properties.forge(string):github(default) orgitlab.host(string or null): HTTPS host; defaults to github.com or gitlab.com.name(string): Name of the repository, e.g. 'acts-project/acts'.display_name(string): Alternative repository name. Useful when fetching one repository multiple times.wip_label(string): Label to identify WIP PRs.show_wip(boolean): If true, WIP PRs will be included in the output, else they are ignored. Default:False.filter_labels(array): If any PR or issue has any label that matches any of these labels, they are excluded. Mutually exclusive withinclude_labelsandexclude_labels.- Items (string)
include_labels(array): If set, only PRs or issues that have all of these labels are included. Mutually exclusive withfilter_labels.- Items (string)
exclude_labels(array): If any PR or issue has any label that matches any of these labels, they are excluded. Mutually exclusive withfilter_labels.- Items (string)
stale_label(string): A label to identify stale PRs/issues. If set, stale PRs and issues will be listed separately and split into newly and other stale items.do_open_prs(boolean): Show a list of open PRs. Default:True.do_merged_prs(boolean): Show a list of merged PRs. Default:True.do_recent_issues(boolean): Show a list of issues opened in the time interval. Default:False.no_assignee_attention(boolean): Draw attention to items without an assignee. Default:True.do_reviewers(boolean): Show reviewer information for PRs. Default:False.show_review_summary(boolean): Show review outcome text (for example,reviewed by). Default:True.truncate_pr_titles(boolean): Fit PR and issue titles on one line across all listings, including review follow-up by replacing the middle with an ellipsis at word boundaries (character fallback for one- or two-word titles), reserving space for status/category badges and the linked item number. Short titles remain unchanged. Default:True; set tofalseto allow wrapping.parse_pr_titles(boolean): Parse conventional-commit prefixes in PR/MR titles. Set tofalsefor repositories that do not use this convention (for example, under[repositories.athena]). Titles retain their prefixes, and title-derived category pills, breaking-change markers, and breaking-change sections are disabled. Keep sectiongroup_by = "none"(the default) to avoid grouping uncategorized items. Default:True.show_pr_categories(boolean): Render parsed categories as pills; requiresparse_pr_titles. Disabling this only hides pills and does not disable prefix parsing. Default:True.group_prs_by_category(boolean): Group open and merged PR slides into one section per category. Default:False. Section pages are titled with the category alone (Feature,Bugfix, ...); thefixcategory also carries a 🐛.pr_category_labels(object): Override display names for category keys used in pills and grouped sections (for example,feat: Feature).pr_category_colors(object): Override LaTeX color names for category pills. Supports keys likefeat,fix,docs,other, andbreaking.show_breaking_changes(boolean): Give merged breaking PRs their own leading section. Default:True. See breaking changes.repeat_breaking_changes(boolean): Keep breaking PRs in the regular merged/category slides as well. Default:True.pr_category_order(array): Order of the grouped category sections. Categories that are not listed follow in the default order.- Items (string)
frame_titles(string or object): Override the title of a kind of frame, either as one string used for every frame or as a map. See frame titles.Spec(object): Cannot contain additional properties.title(string): Deck title shown on the title slide. Supports the placeholders{release},{repos},{since}and{date}. If omitted, mtng uses the default repository-based title.footline_left(string): Left-side text shown in the footline. Supports the placeholders{release},{repos},{since}and{date}. If omitted,mtngis used.accent_color(string): Accent for titles, footline, bullets, and standard blocks in full presentations (--fullor PDF output). Use six hexadecimal digits with an optional#, for exampleaccent_color = "#7C3AED". Defaults to22529E(the existing blue). Fragment output inherits the containing document's theme.repos(array)- Items: Refer to #/definitions/Repository. The equivalent TOML configuration is:
title = "Weekly PR and issue update"
footline_left = "Core team"
[[repos]]
name = "acts-project/acts"
stale_label = "Stale"
wip_label = ":construction: WIP"
show_wip = true
group_prs_by_category = true
pr_category_labels = { feat = "New features", fix = "Bug fixes" }
pr_category_colors = { feat = "LimeGreen", fix = "Purple", breaking = "alertred" }
pr_category_order = ["fix", "feat"]
do_recent_issues = true
no_assignee_attention = true
filter_labels = ["backport"]This configuration will look up the acts-project/acts repository. The output will contain sections on
- Stale PRs and issues. If this is turned on, the
stale_labelkey must be given as well - A list of open PRs, optionally filtered to not include the label given by
wip_label - Merged PRs since the date given by the
--sinceoption - Issues opened since the date given by the
--sinceoption
Conventional-commit prefixes on PR titles (feat:, fix(core)!:, ...) drive the pill next
to each PR and, with group_prs_by_category, the section a PR lands in. The defaults are:
| Key | Label | Section page | Pill color |
|---|---|---|---|
feat |
Feature | 🚀 | LimeGreen |
fix |
Bugfix | 🐛 | Purple |
refactor |
Refactor | ♻️ | CadetBlue |
perf |
Performance | ⚡ | TealBlue |
docs |
Docs | 📚 | CornflowerBlue |
test |
Tests | 🧪 | RoyalBlue |
build |
Build | 🔨 | Goldenrod |
ci |
CI | 🤖 | Orange |
chore |
Chore | ⚙️ | Gray |
revert |
Revert | 🔄 | Mahogany |
other |
Other | 📦 | beige |
Titles that do not parse land in other; a ! marks the PR as breaking with a separate 🚨 marker beside 🆕 (when present).
The category retains its normal color; breaking controls the marker’s text fallback color when emoji fonts are unavailable. The table order is also the order of the grouped sections, and
pr_category_order overrides it — listed categories come first, the rest follow in the
default order:
pr_category_order: [fix, feat]A ! in the prefix (feat!:, fix(core)!:) marks a PR as breaking. Merged breaking PRs are
pulled out of the categories into a --release, released) PRs — open breaking work keeps its red pill where it is.
Two switches control it, per repository:
show_breaking_changes: true # the section itself; default true
repeat_breaking_changes: false # list them ONLY there, not under their categoryWith repeat_breaking_changes left at its default, a breaking PR appears twice: once in the
breaking section and once under its own category. Set it to false to move them out of the
category slides entirely. Its frame title is the breaking_changes key in
frame_titles.
title and footline_left are expanded before rendering, so the release the deck was
generated for can be pulled into the title slide or the footline:
title: ACTS {release}
footline_left: Core team -- {release}| Placeholder | Expands to |
|---|---|
{release} |
The release tag(s) resolved from --release. Empty when not in release mode. |
{repos} |
The repositories in the spec, by display_name where set. |
{since} |
The start of the reporting window (--since), as YYYY-MM-DD. |
{date} |
The generation date, as YYYY-MM-DD. |
{range} |
The reporting window, as between <since> and <now>. |
Unknown keywords are left untouched, so {}-heavy LaTeX in these strings still works. A
placeholder that expands to nothing takes a dangling separator, or an empty pair of
brackets, with it: Core team -- {release} is just Core team outside of release mode.
frame_titles overrides the title of a kind of frame, per repository, using the same
placeholders plus {repo} (the repository's display_name, or its name) and {category}
(the PR category, when group_prs_by_category is on).
A single string is shorthand for "this title, on every frame" — with the placeholders carrying the per-frame detail, that is often all you need:
repos:
- name: acts-project/acts
frame_titles: "{repo} {category}"A map overrides individual frames instead:
repos:
- name: acts-project/acts
frame_titles:
merged_prs: "What landed {range} -- {category}"
open_prs: "{repo}: in flight ({category})"| Key | Default |
|---|---|
breaking_changes |
{repo}: Breaking changes |
merged_prs |
{repo}: PRs merged {range} ({category}) |
release_prs |
{repo}: PRs in release {release} ({category}) |
open_prs |
{repo}: Open PRs ({category}) |
recent_issues |
{repo}: Issues opened since {since} |
new_stale |
{repo}: New stale Issues / PRs since {since} |
all_stale |
{repo}: All stale Issues / PRs |
needs_discussion |
Needs discussion |
stats |
{repo}: period at a glance |
release_stats |
{repo}: release {release} at a glance |
Keys are validated, so a typo is a configuration error rather than a silently ignored
setting. Only the keys you list are overridden; the rest keep their defaults. Because
{category} is empty unless PRs are grouped, the default ({category}) suffix disappears
by itself in ungrouped decks.
In addition and independent of this config, a meeting agenda can be attached at the end if the --event option is provided and contains a valid Indico URL.
If --release is provided, merged PRs are parsed from the release description (for links like https://github.com/<owner>/<repo>/pull/<number>) instead of the --since/--now range, and --since is optional.
You can provide a token explicitly with --token, via GH_TOKEN, or store one in your OS keychain:
$ mtng auth login
GitHub API token: ********
Stored GitHub token for @your-user in your system keychain.mtng auth login validates the token by default. You can also check your current token with:
$ mtng auth check
GitHub token is valid for @your-user.To see where the current token is loaded from:
$ mtng auth status
GitHub token is configured (source: system keychain).By default, the output of mtng generate is a LaTeX fragment. It has to be incorporated into a set of Beamer/LaTeX slides, for example like
$ mtng generate spec.yml > gen.tex
$ mtng generate spec.toml > gen.texwith a LaTeX file like
% Preamble and beginnig of slides
\input{gen.tex}
% Rest of slidesBoth generate and render write plain LaTeX to stdout, without panels, markup
interpretation, or line wrapping, whether stdout is a terminal or redirected.
Progress, compiler logs, and file confirmations go to stderr.
Use --tex report.tex to write the source to a file instead of stdout, or
--pdf report.pdf to compile a PDF without dumping the source. Combine --tex
and --pdf to save both files.
--full controls only the document structure: it produces a standalone LaTeX
document instead of a fragment. PDF compilation automatically selects a complete
document when no custom preamble is supplied.
$ mtng generate spec.yml --full > gen.tex
$ latexmk gen.texIf you have your own deck preamble and still want direct PDF output, pass it with --preamble together with --pdf:
$ mtng generate spec.yml --since 2024-01-01 --preamble mypreamble.tex --pdf notes.pdfIf --preamble points to a full .tex deck file (contains \documentclass or \begin{document}), mtng fails with an explicit error; pass a preamble-only file instead.
This repository is also a reusable action that generates the PDF summary for the project it
runs in, either for a release or for a time period. It installs a LaTeX toolchain, installs
mtng, and runs mtng generate --pdf.
Summarize every release, as it is published:
name: Release summary
on:
release:
types: [published]
jobs:
summary:
runs-on: ubuntu-latest
steps:
- uses: paulgessinger/mtng@v0.8.3
with:
title: "{repos} {release}"
group_prs_by_category: "true"
upload-artifact: "true"The release input defaults to the tag of the release that triggered the workflow, and
repository defaults to the repository the action runs in, so the above needs no further
configuration. Merged PRs are then read from the release description.
Summarize a period instead, for example as a weekly report:
on:
schedule:
- cron: "0 7 * * MON"
jobs:
summary:
runs-on: ubuntu-latest
steps:
- uses: paulgessinger/mtng@v0.8.3
with:
since: 1 week ago
stale_label: Stale
do_recent_issues: "true"
output: reports/weekly.pdf
upload-artifact: "true"Either release or since must be set. since and now accept the same ISO and
human-readable values as the CLI.
The action takes its configuration in one of two ways, and refuses to mix them:
-
Inline, through the inputs named after the config schema fields (
stale_label,wip_label,group_prs_by_category, ...), which describe a single repository. This is what the examples above use. -
From a file, through the
configinput, which points at a checked-in TOML, YAML or JSON config. Use this to report on several repositories, or to share the config with localmtngruns:- uses: actions/checkout@v4 - uses: paulgessinger/mtng@v0.8.3 with: config: .github/mtng.toml since: 1 week ago
The config file is read from the workspace, so the workflow has to check the repository out first.
Combining
configwith any of the inline spec inputs is an error, so a config file is never silently half-overridden.
List-valued inputs (filter_labels, pr_category_order, ...) take one entry per line or a
comma-separated list; map-valued inputs (pr_category_labels, pr_category_colors,
frame_titles) take key=value entries in the same shape:
with:
filter_labels: |
backport
Stale
pr_category_labels: feat=Feature, fix=BugfixBesides the spec fields, the action accepts:
| Input | Default | Description |
|---|---|---|
config |
Path to a config file. Mutually exclusive with the spec inputs. | |
repository |
current repository | Repository to summarize, as owner/name. |
release |
tag of the triggering release | Release tag or URL to summarize. |
since / now |
/ now |
Reporting window. since is required unless release is set. |
event |
Indico event URL to attach as an agenda. | |
output |
mtng-summary.pdf |
Path of the PDF to write. |
tex |
Optional path to also write the LaTeX source to. | |
preamble |
LaTeX preamble file to use instead of the built-in one. | |
upload-artifact |
false |
Upload the PDF as a workflow artifact. |
artifact-name |
mtng-summary |
Name of that artifact. |
artifact-retention-days |
Retention period of that artifact. | |
token |
github.token |
Token used for the GitHub API queries. |
mtng-version |
Version to install from PyPI. Defaults to the source of the action ref itself. | |
install-latex |
true |
Install a LaTeX toolchain with apt. Set to false if the runner or container already has latexmk. |
emoji |
true |
Also install the LuaTeX toolchain and a color emoji font, so emoji render. |
latex-packages |
base toolchain | apt packages to install for LaTeX. |
emoji-packages |
texlive-luatex fonts-noto-color-emoji |
apt packages added when emoji is true. |
Outputs are pdf (the path of the generated PDF) and config (the path of the spec that was
used, generated or given).
Emoji only render under lualatex, which is why emoji pulls in an extra toolchain. Turning it
off gives a smaller, faster pdflatex-only install; the deck then compiles with the emoji left
out, which is also what happens on any TeX installation without an emoji font:
with:
emoji: "false"Run adapter and authentication tests without network access:
uv run --group dev pytest tests/test_forge_gitlab.py tests/test_gitlab_config_auth.py -m "not integration"The fixtures are synthetic examples of documented GitLab REST payloads. For the
optional live smoke test, set GITLAB_TOKEN, MTNG_GITLAB_TEST_PROJECT (a project
path you can read), and optionally MTNG_GITLAB_TEST_HOST (default gitlab.com),
then run uv run --group dev pytest tests/test_forge_gitlab.py -m integration.
uv run --group dev pytest excludes integration by default. Ordinary tests
reject live aiohttp/requests calls and use checked-in payloads; the CI PDF job
renders those fixtures with python tests/render_fixtures.py. Action input
handling is covered by tests/test_action.py without live report collection.
The Live API smoke test workflow is manually dispatched and makes at most
three GitHub requests against this repository. To run it locally, set GH_TOKEN
and MTNG_GITHUB_TEST_PROJECT=owner/repo, then run
uv run --group dev pytest tests/test_live_github.py -m integration -n 1.
Live checks bypass the application cache; cached success is not API validation.
GitHub and GitLab use a persistent JSON HTTP cache under the platform's mtng
cache directory (http-v1/). Keys isolate origins, query parameters, credentials
and representation headers. Responses are reused locally for five minutes by
default, even when the server specifies a shorter lifetime or no-cache.
This deliberate local snapshot can be stale for that window; no-store responses
are never persisted. Set top-level cache_max_age = 300 in TOML (or
cache_max_age: 300 in YAML) to configure the window in seconds. Values must be
non-negative; zero revalidates every time. An explicit --cache-max-age SECONDS
overrides the config, and --refresh forces revalidation regardless of either setting. Expired responses are revalidated using
ETag or Last-Modified when available; validators and bodies are retained for up
to 30 days. Errors never substitute stale successful data. Old model caches are
no longer read.
Use generate --refresh to revalidate immediately, --no-cache to bypass disk
reads/writes, and --cache-stats to print hit, request, revalidation, retry and
shared-request counts. Authentication checks bypass the cache. Cached data may
include private repository content; it stays scoped to the requesting credentials.
Within one process, identical simultaneous requests share a response and requests to GitHub overlap with at most four in flight per origin. Starts are spaced at least 0.2 seconds apart, with search requests spaced at least 2.1 seconds apart. Credential-scoped search/core budgets reserve quota before dispatch, leaving a small safety margin. Throttling pauses the origin and reduces its request rate; GitLab requests remain serialized. Coordination is local to one process, so other clients sharing a token and undisclosed secondary limits can still cause throttling. Rate-limit headers control retry timing, with at most three attempts and a 60-second total wait budget. Longer limits produce an error with retry timing instead of retrying early. CI jobs are independent, so normal CI uses offline fixtures rather than relying on shared private caches.
Save normalized collection results alongside the first rendered report:
mtng generate report.toml --since "1 week ago" --save-data report-data.json --tex report.tex
mtng render report-data.json --full --pdf report.pdf
mtng render report-data.json --config presentation.toml --tex revised.texrender uses no credentials, keychain or HTTP session. Snapshots contain a
versioned JSON schema, the original configuration/time window, normalized query
results and optional Indico contributions. They contain no API credentials or
HTTP headers, but can contain private report content; share them accordingly.
Unknown schema versions are rejected.
The optional configuration override can change titles, grouping, display names and presentation flags or omit sections. Requesting new repositories, filters, sections or enrichment that were not collected fails with instructions to collect a new snapshot. Rendering cannot silently fetch missing data or invent it.
Open PRs/MRs fetch reviews only when their review summary is displayed. GitHub also skips per-PR detail requests when neither reviews nor details are needed. Merged/release data still fetches the information needed for the statistics tiles. This separates collection requirements from presentation without changing report modes or adding more mode flags.
Interactive terminals show section/repository progress and completed/total counts for PR/MR details and reviews. Searches count discovered items as pages arrive, without guessing the final total. API retry waits identify the host and wait time. LaTeX rendering and compilation retain a status indicator because their total work is not known. Progress goes to stderr; redirected and CI runs receive plain stage and completion messages instead of animated bars. Failed work is marked stopped, not complete.
PR bullets show additions/deletions and files changed when the backend supplies
those counts. Open PRs show human-readable ages (for example, open for 18 days or
opened today), measured at the report date. Inactivity is shown only after at
least one calendar day. Drafts use a muted icon and a Draft badge. With do_reviewers = true and show_review_summary = true, PRs show a compact, consistently sized review status without reviewer usernames;
changes requested takes precedence over approval, using each reviewer's latest
decisive review. These are review summaries, not mergeability checks. Include
drafts with drafts = "include" on a changes section. Older saved reports may
need recollecting to supply review and diff details.
Open and merged PRs created within the inclusive since–now reporting window carry a NEW
badge. Metadata wraps within the list width, and PR bullets stay together when
continued onto the next slide.
Add this top-level table to your report TOML (or a separate config overlay):
[llm]
enabled = true
max_diff_chars = 20000
max_description_chars = 8000
timeout = 120 # seconds per summary
max_concurrency = 4 # summaries generated in parallel, bounded by thisChoose one of two transports with llm.transport.
command (the default) runs a local executable per summary, feeding the
prompt on stdin and reading the summary from stdout:
[llm.transport]
type = "command"
command = ["claude", "-p", "--output-format", "text"]
# or, e.g., command = ["llm"]chat-completions POSTs to any OpenAI-compatible chat completions endpoint,
such as a LiteLLM gateway, Ollama, vLLM or OpenRouter:
[llm.transport]
type = "chat-completions"
model = "anthropic/claude-sonnet-4" # gateway model name
base_url = "https://llm-gateway.example.com/v1"
api_key_env = "LITELLM_API_KEY" # name of the environment variable holding the key
# identity = "team-b" # optional, see below
# temperature = 0.2
# max_tokens = 200The key is resolved like forge tokens: first from the environment variable
named by api_key_env, then from the system keychain (service mtng, entry
llm:<host>:token, e.g. llm:llm-gateway.example.com:token). If neither is
present, the request is sent without an Authorization header, which suits
keyless local gateways. LiteLLM virtual keys are scoped to a team; hold keys
for several teams on one gateway by storing one keychain entry per team and
naming them with identity, which selects llm:<host>:<identity>:token:
$ mtng auth llm login https://llm-gateway.example.com/v1 --identity team-b
$ mtng auth llm status https://llm-gateway.example.com/v1 --identity team-b --validateThe prompt includes the PR title, description and diff;
length limits count characters and truncated inputs are marked. Setting
max_diff_chars = 0 skips fetching diffs. GitHub PRs and GitLab MRs are supported.
Distinct PRs are summarized once (even across sections) with at most
max_concurrency requests in flight; set it to 1 for strictly sequential
summarization.
Summaries cover every section by default. Each repository section accepts an
llm_summary flag to narrow this: set llm_summary = false to exclude one
section, or set llm_summary = true on any section to switch to opt-in mode,
where only sections marked true are summarized.
The review_followup section additionally accepts strings or lists of strings,
selecting individual follow-up groups by kind — useful when only some kinds of
follow-up benefit from summaries:
[[sections]]
type = "review_followup"
repositories = ["acts"]
assignment_source = "both"
llm_summary = "needs_assignment" # or a list of kindsValid kinds are needs_assignment, awaiting_author, awaiting_review,
changes_since_review, needs_clarification, needs_maintainer_attention and
review_status_unknown; unknown names are rejected when the configuration is
loaded. A kind selection also counts as opting the section in.
The changes section accepts llm_summary = "new", restricting summaries to
items created within the report window — the same items the report marks as
new. This also counts as opting the section in:
[[sections]]
type = "changes"
state = "open"
repositories = ["acts"]
llm_summary = "new"PRs appearing in several sections are still summarized once, but displayed
only in the selected sections. A PR follows its group's selection: kinds with
per-actor groups (such as awaiting_review) can appear in sibling groups of
a different kind, which then shows the summary there too.
The default instructions request one short plain-text sentence of at most 15
words. Override them with llm.instructions. Summaries appear beneath the PR
metadata and are escaped for LaTeX. Transport failures, timeouts, HTTP errors and
empty output stop generation and are not cached; results already returned are
cached as they arrive, so an aborted run resumes where it stopped.
By default (title_check = true) the model also judges whether the title alone
is sufficiently descriptive: it prefixes its reply with USEFUL or REDUNDANT,
and summaries judged redundant with the title are omitted from the slides. Set
title_check = false to keep every summary. Enabling
this changes every prompt, so existing cached summaries are re-generated once.
Successful results are cached indefinitely by the exact prompt and transport
configuration, in llm-v1 inside mtng's user cache directory (normally
~/.cache/mtng/llm-v1 on Linux). There is no automatic eviction; remove that
directory to clear summaries. API cache flags do not clear this separate cache.
Diffs are fetched again when collecting to detect changes before reusing a
summary. --save-data includes summaries; offline render reuses those saved
summaries without invoking a transport or fetching diffs. Recollect to generate
summaries with changed LLM settings. Omit [llm] or set enabled = false to
leave summaries disabled. In the GitHub Action, supply LLM settings through the
config file; for the command transport, install your chosen command on the
runner, and for chat-completions, provide the API key environment variable.
Use a review_followup section to group non-draft PRs/MRs by likely next action.
These are hints from review state, not definitive task assignments:
- Awaiting author: username: a reviewer requested changes.
- Awaiting review: username: a selected assignee/requested reviewer has not participated. PRs with new commits since a submitted review are listed at the end of the same frame under Re-review, assigned to whichever reviewer with an outdated review reviewed most recently (group kind
changes_since_review). - Review comments, no decision: review participation is complete, but only comment reviews exist and nothing was pushed since. The next step may lie with the author or the reviewer (group kind
needs_clarification). - Needs maintainer attention (approved): review participation is complete and an approval exists. Mergeability, CI, and branch requirements are not checked.
- Needs assignment: no selected assignees/requested reviewers and no submitted external review.
- Review status unknown: review data is missing or incomplete.
Requested changes take priority over approvals and outstanding requests. For each
reviewer, the latest approval, changes request, or dismissal determines their decision;
a later comment or pending review does not revoke it. Reviews are ordered by submission
time, falling back to their input order when timestamps are missing. Author reviews
are ignored. On GitHub, the latest submitted review's commit_id is compared with
head.sha, including comment-only reviews and approvals. A changed head routes the
PR to Re-review rather than author action
when their changes request predates that head. A changes request without evidence
of a changed head still takes priority. This includes rebases and force-pushes; it
is not proof that the feedback was addressed or that the diff changed semantically.
With review_completion = "any", another person's review of the current head satisfies
participation, but an outstanding changes request still needs its original reviewer's
attention. Missing commit IDs retain the previous state-based classification.
No commit-date inference is made from updated_at.
[repositories.project]
name = "example/project"
[[sections]]
type = "review_followup"
repositories = ["project"]
assignment_source = "both"Set assignment_source = "assignees", "requested_reviewers", or "both" on the
section to choose who counts as assigned. The default is "both". A PR needs assignment
when the selected source contains nobody. A section without an override inherits the
repository’s review_assignment_source.
For legacy [[repos]] configurations, set do_review_followup = true. Use
review_assignment_source on the repository to choose the source. This works
independently of do_open_prs and do_reviewers. Repository or section label filters
still apply. Native drafts and PRs with the configured WIP label are excluded. Entries
are sorted by last update, longest inactive first within each group, and show both days
without updates and PR age, plus the linked author, additions/deletions, and changed-file
count when available. Last update uses the forge’s updated_at timestamp; this
includes metadata changes and is not a precise measure of human interaction or time
since assignment. A section with nothing outstanding shows an all-clear message.
By default (review_completion = "all"), each assignee and requested reviewer must
submit their own approval, changes request, or comment-only review. Set
review_completion = "any" on a review_followup section when one review from
anyone other than the PR author is enough to move the PR from outstanding reviews
to the appropriate next-action group. Sections inherit the repository's review_completion
unless overridden. Submitted reviews still count after GitHub removes a fulfilled
review request. With either policy, a PR with a submitted non-author review and no
remaining selected assignments does not need assignment.
Pending and dismissed reviews do not count. Ordinary conversation
comments do not count. This does not establish whether a review happened after
assignment. People with outdated reviews can appear even after their request disappears. An earlier submitted review counts
even if a new review was requested later; tracking re-review requests requires
additional history.
Missing or partial review data appears under Review status unknown unless a known changes request identifies author action. GitLab currently supplies approvals only, so it cannot establish the absence of changes requests. Recollect older saved reports to capture all assignees and the required review data, including commit IDs for changes-since-review detection. GitLab currently lacks reviewed-commit IDs, so that detection is unavailable there.
Generate a fictional example with uv run python examples/review_followup.py.


