Skip to content

Commit b8747ad

Browse files
edimarlnxclaude
andauthored
Add --report, and a bug form that asks for it (#10)
* Add --report, and a bug form that asks for it Every bug report from a machine nobody here owns starts with the same three questions: which version, against which engine, on what. --report answers all of them in one block, so a reporter pastes rather than remembers. The block comes from the kit's new report package, so the whole family answers in the same shape. What this tool adds is its own half: which engines this machine has and at which versions, or the reason a version could not be read. That line answers most reports here on its own — an action refused by a Podman below 5.0 and one refused by a bug look exactly the same from the outside. It reaches no engine socket and it runs before the backend is built. A user who cannot reach the socket, or a machine with neither engine installed, still produces a report. In --demo it says demo on the backend line, names the backend the fake imitates, and says plainly that nothing was probed. The block is written to be published: no hostname, no user name, no home path. The unit test asserts that, and the smoke test asserts it again on a real guest, next to the checks that the report agrees with the backend --check names and with the engines the guest really has. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCypLQF5AD8JD831Ea9S9q * Keep the leak check off the two lines that never carry a node name The report's privacy check greps its whole output for the host name, which a machine named after its distribution fails on a line it is entitled to: the distro line comes from /etc/os-release and the kernel line from uname's release and machine, never from the node name. Both are excluded from that search, and from that search only — every other line is still checked, which is where a host name would actually leak. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCypLQF5AD8JD831Ea9S9q --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent a41ada5 commit b8747ad

8 files changed

Lines changed: 410 additions & 27 deletions

File tree

‎.github/ISSUE_TEMPLATE/bug_report.yml‎

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,79 @@
1+
# Bug report form for tui-containers.
2+
#
3+
# Rendered from tui-kit/templates/ISSUE_TEMPLATE/bug_report.yml — every tool in
4+
# the family asks the same questions in the same order, so edit it there and
5+
# render again rather than editing this copy.
6+
#
7+
# The first field is the whole point: `tui-containers --report` prints the tool and
8+
# kit versions, the backend and its version, the distribution, the kernel, the
9+
# terminal and where the binary came from, in one block that is safe to
10+
# publish. Asking for it once, in a fixed shape, is what stops a report from
11+
# turning into three rounds of "which version, on what?".
112
name: Bug report
213
description: Something the tool got wrong on a real machine
314
labels: ["bug"]
415
body:
516
- type: markdown
617
attributes:
718
value: |
8-
Thanks for the report. The most useful bug reports here quote the
9-
output the tool showed and the output of the command it drives.
19+
Thanks for the report. The two things that make one useful here are the
20+
`--report` block below and the exact steps that reproduce it.
21+
- type: textarea
22+
id: report
23+
attributes:
24+
label: Output of `tui-containers --report`
25+
description: |
26+
Run `tui-containers --report` and paste the output verbatim, without editing
27+
it. It names the versions, the backend, the distribution, the kernel
28+
and the terminal — and it deliberately carries no hostname, user name,
29+
home path or address, so it is safe to publish as it is.
30+
render: shell
31+
validations:
32+
required: true
1033
- type: textarea
1134
id: what
1235
attributes:
1336
label: What happened
14-
description: What you did, what you expected, what you got instead.
37+
description: What the tool did, in your own words.
1538
validations:
1639
required: true
1740
- type: textarea
18-
id: version
41+
id: steps
1942
attributes:
20-
label: Versions
43+
label: Steps to reproduce
2144
description: |
22-
Output of `tui-template --version`, your distribution and release, and the
23-
version of the command the tool drives.
24-
render: shell
45+
The keystrokes or the command line, from starting the tool to the
46+
moment it went wrong. If the state of the machine matters, say what it
47+
was.
48+
placeholder: |
49+
1. Start `tui-containers`
50+
2. …
51+
3. …
2552
validations:
2653
required: true
54+
- type: input
55+
id: expected
56+
attributes:
57+
label: What you expected
58+
description: "One line: what should have happened instead."
59+
validations:
60+
required: true
61+
- type: checkboxes
62+
id: demo
63+
attributes:
64+
label: Does it reproduce in demo mode?
65+
description: |
66+
`--demo` runs against sample data and never touches the system, so a
67+
bug that shows up there is one anybody can reproduce on any machine.
68+
options:
69+
- label: I tried `tui-containers --demo`, and it reproduces there too
70+
- label: I tried `tui-containers --demo`, and it does not reproduce there
2771
- type: textarea
2872
id: output
2973
attributes:
3074
label: Relevant output
3175
description: |
3276
Anything the tool printed, plus the raw output of the underlying
33-
command if a parse looks wrong. Redact addresses you would rather not
34-
publish.
77+
command if a parse looks wrong. Redact anything you would rather not
78+
publish — addresses, host names, rule comments.
3579
render: shell
36-
- type: checkboxes
37-
id: demo
38-
attributes:
39-
label: Does it reproduce in demo mode?
40-
options:
41-
- label: I tried `tui-template --demo`

‎README.md‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ you.
347347
tui-containers # drive the real machine
348348
tui-containers --demo # sample machine, no privileges needed
349349
tui-containers --check # read every engine, print JSON, exit
350+
tui-containers --report # print what a bug report needs, exit
350351
tui-containers --theme ~/mytheme/colors.toml
351352
tui-containers --sudo "" # never escalate; root's podman is then not listed
352353
tui-containers --version
@@ -399,6 +400,47 @@ against real machines on Ubuntu, Fedora and Arch; the assertions live in
399400
[`test/smoke.sh`](test/smoke.sh), every one of them is read-only, and **none of
400401
them pulls an image**.
401402

403+
### `--report`, for bug reports
404+
405+
`--report` prints, in one block, everything a maintainer has to ask for
406+
otherwise: the tool and kit versions, which engines this machine has and at
407+
which versions, the distribution, the kernel, the terminal, the theme, the
408+
escalation prefix, and whether the running binary came from a package. It needs
409+
no privileges and reads no engine — it asks each one for its version and
410+
nothing else — so it works on the machine where the bug is, including one where
411+
the socket cannot be reached at all, which is itself a thing worth reporting.
412+
413+
```console
414+
$ tui-containers --report
415+
tui-containers 0.1.2 (kit v0.2.9)
416+
backend: host (version unknown: the machine itself; the engine versions are on the engines line)
417+
mode: live
418+
distro: fedora 42 (Fedora Linux 42 (Workstation Edition))
419+
kernel: 6.19.14-108.fc42.x86_64
420+
arch: x86_64
421+
locale: en_US.UTF-8
422+
term: xterm-256color
423+
theme: tokyo-night
424+
sudo: sudo -n
425+
root: no
426+
binary: /usr/bin/tui-containers (packaged)
427+
engines: docker 29.5.3, podman 5.8.2
428+
```
429+
430+
The engines line is the one that answers most reports: an action refused by a
431+
Podman below 5.0 and one refused by a bug look exactly the same from the
432+
outside. An engine that is not installed, or whose version could not be read,
433+
is named there with the reason rather than left out.
434+
435+
The block is written to be published as it is: it carries no hostname, user
436+
name, home path or address, and no environment variable beyond `LANG`,
437+
`LC_ALL`, `TERM` and `TERM_PROGRAM`. A binary living under your home directory
438+
is reported as being there without naming the path. `--report` works with
439+
`--demo` too, where it says so on the `mode` line.
440+
441+
The bug form asks for this block first — see
442+
[`.github/ISSUE_TEMPLATE/bug_report.yml`](.github/ISSUE_TEMPLATE/bug_report.yml).
443+
402444
## What it can do to your machine
403445

404446
Every one of these is previewed and confirmed first.

‎cmd/tui-containers/main.go‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func defaults() map[string]string {
4646
type options struct {
4747
demo bool
4848
check bool
49+
report bool
4950
themePath string
5051
sudo string
5152
showVersion bool
@@ -64,6 +65,7 @@ func parseFlags(args []string, out *os.File) (options, error) {
6465
fs.BoolVar(&opts.check, "check", false,
6566
"read the engines and print the parsed state as JSON, then exit "+
6667
"(no UI, no changes); exit 1 if no engine can be read")
68+
fs.BoolVar(&opts.report, "report", false, reportUsage)
6769
fs.StringVar(&opts.themePath, "theme", "",
6870
"path to an Omarchy-style colors.toml (overrides the config file)")
6971
fs.StringVar(&opts.sudo, "sudo", "",
@@ -117,6 +119,24 @@ func run(args []string) error {
117119
}
118120
applyOverrides(&cfg, opts)
119121

122+
// The configured theme is handed to the kit through the same variable the
123+
// user could set by hand, so precedence stays in one place. It is set
124+
// before the backend is built so --report can name the theme the UI would
125+
// have used even on a machine where no backend can be.
126+
if path := cfg.Theme(); path != "" {
127+
if err := os.Setenv("TUI_THEME", path); err != nil {
128+
return err
129+
}
130+
}
131+
132+
// --report is the non-interactive path that must work everywhere. It
133+
// reads no engine and it survives a machine with neither of them, because
134+
// "there is no engine here" is one of the things a bug report has to be
135+
// able to say. So it comes before the backend is built.
136+
if opts.report {
137+
return runReport(cfg, opts, os.Stdout)
138+
}
139+
120140
// The engine versions are probed once, before the backend is built,
121141
// because the backend needs the capability sets: whether this Docker
122142
// understands `--format json` and whether this Podman can change a restart
@@ -135,14 +155,6 @@ func run(args []string) error {
135155
return runCheck(backend, backendCompat, os.Stdout)
136156
}
137157

138-
// The configured theme is handed to the kit through the same variable the
139-
// user could set by hand, so precedence stays in one place.
140-
if path := cfg.Theme(); path != "" {
141-
if err := os.Setenv("TUI_THEME", path); err != nil {
142-
return err
143-
}
144-
}
145-
146158
program := tea.NewProgram(newApp(backend, theme.New(), backendCompat),
147159
tea.WithAltScreen())
148160
_, err = program.Run()

‎cmd/tui-containers/report.go‎

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"strings"
8+
9+
"github.com/tui-tools/tui-kit/compat"
10+
"github.com/tui-tools/tui-kit/config"
11+
"github.com/tui-tools/tui-kit/report"
12+
"github.com/tui-tools/tui-kit/theme"
13+
)
14+
15+
// hostDetail explains the one line of the block that would otherwise read as a
16+
// failed probe. This tool's backend is the machine itself, and a machine has no
17+
// version: the versions that matter are the engines', and they get their own
18+
// line right below.
19+
const hostDetail = "the machine itself; the engine versions are on the engines line"
20+
21+
// runReport prints the block a bug report needs and exits. Everything generic
22+
// — the kit version, the distribution, the kernel, the terminal, where the
23+
// binary came from — is collected by the kit, so the whole family answers
24+
// --report in the same shape. What this function adds is the part only
25+
// tui-containers knows: which engines this machine has and at which versions,
26+
// which is what decides nearly every question asked of a report here — an
27+
// action refused by a Podman below 5.0 and one refused by a bug look the same
28+
// from the outside.
29+
//
30+
// It reads no engine. --check is the flag that does that, and it needs a socket
31+
// this account may not be able to reach; a report has to work for a user who
32+
// cannot reach it, because the unreachable socket may be the bug. A machine
33+
// with neither engine installed still gets a report, with "none" on the engines
34+
// line: "there is nothing here to drive" is a bug report, not a refusal.
35+
func runReport(cfg config.Config, opts options, out io.Writer) error {
36+
palette, _ := theme.ResolvePalette()
37+
38+
// The same probe the header and --check use. There is one version probe in
39+
// this tool and this is it. It runs `docker --version` and
40+
// `podman --version`, which read nothing and change nothing.
41+
results := probeCompat(context.Background(), opts.demo)
42+
43+
var backendName, selectError string
44+
if backend, err := pickBackend(cfg, opts, results); err != nil {
45+
selectError = err.Error()
46+
} else {
47+
backendName = backend.Name()
48+
}
49+
50+
info := report.Info{
51+
Tool: toolName,
52+
Version: version,
53+
Backend: backendName,
54+
BackendDetail: hostDetail,
55+
Demo: opts.demo,
56+
Sudo: cfg.String(config.KeySudo, ""),
57+
Theme: palette.Name,
58+
}
59+
if opts.demo {
60+
// The fake stands in for the same backend the real one is, and it
61+
// answers to the same name; naming it on its own line keeps the
62+
// backend line free to say demo, which is the thing a reader must not
63+
// miss.
64+
info.Backend = "demo"
65+
imitated := backendName
66+
if imitated == "" {
67+
imitated = "host"
68+
}
69+
info.Extra = append(info.Extra, report.Field{
70+
Key: "demo backend", Value: imitated,
71+
})
72+
}
73+
info.Extra = append(info.Extra, report.Field{
74+
Key: "engines", Value: describeEngines(results, opts.demo),
75+
})
76+
if selectError != "" {
77+
info.Extra = append(info.Extra, report.Field{
78+
Key: "backend error", Value: selectError,
79+
})
80+
}
81+
82+
_, err := io.WriteString(out, report.Render(info))
83+
return err
84+
}
85+
86+
// describeEngines renders every engine this tool declares, with the version it
87+
// answered or the reason it did not, as one line. A report that named only the
88+
// engine that answered would leave the reader guessing whether the other was
89+
// absent or merely unreadable from this account, and that difference is most of
90+
// the "nothing is listed" reports.
91+
func describeEngines(results []compat.Result, demo bool) string {
92+
if demo {
93+
// --demo drives an in-memory machine, so nothing was probed. "none"
94+
// here would read as a machine with no engine installed.
95+
return "not probed (demo reads no machine)"
96+
}
97+
parts := make([]string, 0, len(results))
98+
for _, result := range results {
99+
name := strings.TrimSpace(result.Backend)
100+
if name == "" {
101+
continue
102+
}
103+
switch {
104+
case strings.TrimSpace(result.Version) != "":
105+
parts = append(parts, name+" "+strings.TrimSpace(result.Version))
106+
case strings.TrimSpace(result.Detail) != "":
107+
parts = append(parts, name+" (no version: "+result.Detail+")")
108+
default:
109+
parts = append(parts, name+" (no version)")
110+
}
111+
}
112+
if len(parts) == 0 {
113+
return "none"
114+
}
115+
return strings.Join(parts, ", ")
116+
}
117+
118+
// reportUsage is the flag's one-line help, kept here next to what it prints.
119+
var reportUsage = fmt.Sprintf(
120+
"print the versions and machine facts a bug report needs, then exit "+
121+
"(no UI, no privileges, nothing about you: paste it into a %s issue)",
122+
toolName)

0 commit comments

Comments
 (0)