Skip to content
paulgessingerPublic

About

CLI tool written in Python to build beautiful LaTeX slides summarizing activity on GitHub repositories

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Repository files navigation

mtng

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:

Screenshot of the tool's output

Issue and stale-item slides use the same compact layout as PR slides:

Example stale issues and PRs slide

Installation

pip install mtng

Interface

$ 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.

Configuration

mtng consumes a configuration file to specify repositories and report sections. YAML (.yml / .yaml), TOML (.toml) and JSON (.json) are all supported.

Ordered sections (TOML first)

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.

Shared configs and overlays

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.tex

Files 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 configurations

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:
      - backport

GitLab and mixed-host reports

The 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 = true

Use 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.tex

auth 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.

Schema

  • Repository (object): Cannot contain additional properties.
    • forge (string): github (default) or gitlab.
    • 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 with include_labels and exclude_labels.
      • Items (string)
    • include_labels (array): If set, only PRs or issues that have all of these labels are included. Mutually exclusive with filter_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 with filter_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 to false to allow wrapping.
    • parse_pr_titles (boolean): Parse conventional-commit prefixes in PR/MR titles. Set to false for 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 section group_by = "none" (the default) to avoid grouping uncategorized items. Default: True.
    • show_pr_categories (boolean): Render parsed categories as pills; requires parse_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, ...); the fix category 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 like feat, fix, docs, other, and breaking.
    • 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, mtng is used.
      • accent_color (string): Accent for titles, footline, bullets, and standard blocks in full presentations (--full or PDF output). Use six hexadecimal digits with an optional #, for example accent_color = "#7C3AED". Defaults to 22529E (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

  1. Stale PRs and issues. If this is turned on, the stale_label key must be given as well
  2. A list of open PRs, optionally filtered to not include the label given by wip_label
  3. Merged PRs since the date given by the --since option
  4. Issues opened since the date given by the --since option

PR categories

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]

Breaking changes

A ! in the prefix (feat!:, fix(core)!:) marks a PR as breaking. Merged breaking PRs are pulled out of the categories into a ⚠️ section of their own, ahead of the category sections, so they lead the deck. The section is skipped when nothing broke, and only covers merged (or, with --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 category

With 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.

Placeholders

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

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.

Authentication

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).

Making a presentation

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.tex

with a LaTeX file like

% Preamble and beginnig of slides
\input{gen.tex}
% Rest of slides

Both 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.tex

If 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.pdf

If --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.

GitHub Action

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.

Configuration

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 config input, which points at a checked-in TOML, YAML or JSON config. Use this to report on several repositories, or to share the config with local mtng runs:

    - 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 config with 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=Bugfix

Action inputs

Besides 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"

GitLab development tests

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.

Offline tests and live smoke checks

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.

API response cache

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 once, render offline

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.tex

render 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.

Collection progress

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.

Optional LLM summaries

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 this

Choose 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 = 200

The 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 --validate

The 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 kinds

Valid 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.

Review follow-up

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.

Review follow-up example

About

CLI tool written in Python to build beautiful LaTeX slides summarizing activity on GitHub repositories

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages