Skip to content

Commit f411e9d

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 backend, on what. --report answers all of them in one block, so a reporter pastes rather than remembers. The block comes from the kit's report package, so the whole family answers in the same shape. What this tool adds is its own half: the backend that was selected, the version the same probe --check uses read off `ssh -V`, and which of the manifest's features that version has — the keyword this tool would write is a version question, and a bug about a change that did not take is answered on that line. It reads no configuration and starts no sshd, and it runs before the backend is required. A user who cannot escalate, or a machine with no OpenSSH at all, still produces a report, with the selection error as one of its lines. In --demo it says demo on the backend line, names OpenSSH as what the fake imitates, and says the features were not probed rather than claiming them all. The block is written to be published: nothing about the server — no host key, no address, no port, no setting — and nothing about the user. The smoke test asserts that on a real guest, next to the checks that the report agrees with the backend the machine should be driving. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCypLQF5AD8JD831Ea9S9q * The leak check searches every line but the two uname never names `grep -cE '/home/|$(uname -n)'` over the whole block has a false positive waiting on most guests: a machine called `fedora` or `ubuntu` has its host name as a substring of the `distro:` line, which is built from /etc/os-release and never from uname's nodename. The kernel line is the same story — release and machine, never the node. Excluding those two lines from the host-name search keeps the promise on everything the report actually assembles. 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 7de9b8f commit f411e9d

8 files changed

Lines changed: 374 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-ssh.
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-ssh --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-ssh --report`
25+
description: |
26+
Run `tui-ssh --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-ssh`
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-ssh --demo`, and it reproduces there too
70+
- label: I tried `tui-ssh --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: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ every client that has ever connected.
313313
tui-ssh # drive the real server
314314
tui-ssh --demo # sample server, no privileges needed
315315
tui-ssh --check # read the server, print JSON, exit
316+
tui-ssh --report # print what a bug report needs, exit
316317
tui-ssh --theme ~/mytheme/colors.toml
317318
tui-ssh --sudo "" # run the commands directly (as root)
318319
tui-ssh --version
@@ -352,6 +353,44 @@ permitted, that the tool said so instead of showing an empty screen.
352353
against real machines on Ubuntu, Fedora and Arch; the assertions live in
353354
[`test/smoke.sh`](test/smoke.sh), and every one of them is read-only.
354355

356+
### `--report`, for bug reports
357+
358+
`--report` prints, in one block, everything a maintainer has to ask for
359+
otherwise: the tool and kit versions, the backend and the version probed off
360+
`ssh -V`, which of the keyword spellings that OpenSSH has, the distribution,
361+
the kernel, the terminal, the theme, the escalation prefix, and whether the
362+
running binary came from a package. It needs no privileges and reads neither
363+
the configuration nor the service, so it works on the machine where the bug is
364+
— including one with no OpenSSH installed at all, where the selection error is
365+
itself one of the lines.
366+
367+
```console
368+
$ tui-ssh --report
369+
tui-ssh 0.1.0 (kit v0.2.9)
370+
backend: openssh 9.9
371+
mode: live
372+
distro: fedora 42 (Fedora Linux 42 (Workstation Edition))
373+
kernel: 6.19.14-108.fc42.x86_64
374+
arch: x86_64
375+
locale: en_US.UTF-8
376+
term: xterm-256color
377+
theme: tokyo-night
378+
sudo: sudo -n
379+
root: no
380+
binary: /usr/bin/tui-ssh (packaged)
381+
features: include-dropins yes, kbd-interactive yes
382+
```
383+
384+
The block is written to be published as it is. It carries nothing about your
385+
server — no host key, no address, no port, no setting — and nothing about you:
386+
no hostname, no user name, no home path, and no environment variable beyond
387+
`LANG`, `LC_ALL`, `TERM` and `TERM_PROGRAM`. A binary living under your home
388+
directory is reported as being there without naming the path. `--report` works
389+
with `--demo` too, where it says so on the `mode` line.
390+
391+
The bug form asks for this block first — see
392+
[`.github/ISSUE_TEMPLATE/bug_report.yml`](.github/ISSUE_TEMPLATE/bug_report.yml).
393+
355394
## What it can do to your machine
356395

357396
Every one of these is previewed and confirmed first.

‎cmd/tui-ssh/main.go‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func defaults() map[string]string {
4444
type options struct {
4545
demo bool
4646
check bool
47+
report bool
4748
themePath string
4849
sudo string
4950
showVersion bool
@@ -62,6 +63,7 @@ func parseFlags(args []string, out *os.File) (options, error) {
6263
fs.BoolVar(&opts.check, "check", false,
6364
"read the server and print the parsed state as JSON, then exit "+
6465
"(no UI, no changes); exit 1 if the backend cannot be read")
66+
fs.BoolVar(&opts.report, "report", false, reportUsage)
6567
fs.StringVar(&opts.themePath, "theme", "",
6668
"path to an Omarchy-style colors.toml (overrides the config file)")
6769
fs.StringVar(&opts.sudo, "sudo", "",
@@ -120,6 +122,25 @@ func run(args []string) error {
120122
// manifest.
121123
backendCompat := probeCompat(context.Background(), opts.demo)
122124

125+
// The configured theme is handed to the kit through the same variable the
126+
// user could set by hand, so precedence stays in one place. It is set
127+
// before the backend is built so --report can name the theme the UI would
128+
// have used even on a machine where no backend can be.
129+
if path := cfg.Theme(); path != "" {
130+
if err := os.Setenv("TUI_THEME", path); err != nil {
131+
return err
132+
}
133+
}
134+
135+
// --report is the non-interactive path that must work everywhere. It reads
136+
// nothing privileged and it survives a machine with no OpenSSH at all,
137+
// because "there is nothing here to drive" is one of the things a bug
138+
// report has to be able to say. So it comes before the backend is
139+
// required.
140+
if opts.report {
141+
return runReport(cfg, opts, os.Stdout)
142+
}
143+
123144
backend, err := pickBackend(cfg, opts, backendCompat)
124145
if err != nil {
125146
return err
@@ -131,14 +152,6 @@ func run(args []string) error {
131152
return runCheck(backend, backendCompat, os.Stdout)
132153
}
133154

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

‎cmd/tui-ssh/report.go‎

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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+
"github.com/tui-tools/tui-ssh/internal/openssh"
14+
)
15+
16+
// runReport prints the block a bug report needs and exits. Everything generic
17+
// — the kit version, the distribution, the kernel, the terminal, where the
18+
// binary came from — is collected by the kit, so the whole family answers
19+
// --report in the same shape. What this function adds is the part only tui-ssh
20+
// knows: the backend it selected, the OpenSSH version the same probe --check
21+
// uses read off `ssh -V`, and which of the manifest's features that version
22+
// has — because the keyword this tool would write is a version question, and a
23+
// bug about the wrong spelling is answered on that line.
24+
//
25+
// It reads no configuration and starts no sshd. --check is the flag that does
26+
// that, and it escalates; a report has to work for a user who cannot, because
27+
// the missing privilege may be the bug. For the same reason a machine with no
28+
// OpenSSH at all still gets a report, with the selection error as one of its
29+
// lines: "there is nothing here to drive" is a bug report, not a refusal.
30+
func runReport(cfg config.Config, opts options, out io.Writer) error {
31+
palette, _ := theme.ResolvePalette()
32+
33+
// The same probe --check and the header use. There is one version probe in
34+
// this tool and this is it, and the backend is built from its capability
35+
// set, so it comes first here as it does in run.
36+
backendCompat := probeCompat(context.Background(), opts.demo)
37+
38+
var selected, selectError string
39+
if backend, err := pickBackend(cfg, opts, backendCompat); err != nil {
40+
selectError = err.Error()
41+
} else {
42+
selected = backend.Name()
43+
}
44+
45+
info := report.Info{
46+
Tool: toolName,
47+
Version: version,
48+
Backend: selected,
49+
BackendVersion: backendCompat.Version,
50+
BackendDetail: backendCompat.Detail,
51+
Demo: opts.demo,
52+
Sudo: cfg.String(config.KeySudo, ""),
53+
Theme: palette.Name,
54+
}
55+
if opts.demo {
56+
// The fake answers to the same name as the real backend, so the block
57+
// would otherwise read as a live server. What it imitates is OpenSSH,
58+
// which is the parsers and the command builders the session exercised.
59+
info.Backend = "demo"
60+
info.Extra = append(info.Extra, report.Field{
61+
Key: "demo backend", Value: backendName,
62+
})
63+
}
64+
info.Extra = append(info.Extra, report.Field{
65+
Key: "features", Value: describeFeatures(backendCompat.Caps(), opts.demo),
66+
})
67+
if selectError != "" {
68+
info.Extra = append(info.Extra, report.Field{
69+
Key: "backend error", Value: selectError,
70+
})
71+
}
72+
73+
_, err := io.WriteString(out, report.Render(info))
74+
return err
75+
}
76+
77+
// reportedFeatures are the manifest features whose answer changes what this
78+
// tool writes, in the order the block names them.
79+
var reportedFeatures = []string{
80+
openssh.FeatureIncludeDropins,
81+
openssh.FeatureKbdInteractive,
82+
}
83+
84+
// describeFeatures renders the probe's verdict on each of them as one line. A
85+
// report that says only "openssh 8.4" leaves the reader working out for
86+
// themselves which keyword spelling that release accepts, and that difference
87+
// is most of the bugs about a change that did not take.
88+
func describeFeatures(caps compat.Caps, demo bool) string {
89+
// Nothing was probed in demo mode, and an unprobed capability set answers
90+
// yes to everything by design — which would read here as a verdict about
91+
// the machine rather than as the absence of one.
92+
if demo {
93+
return "not probed (the fake accepts every spelling)"
94+
}
95+
parts := make([]string, 0, len(reportedFeatures))
96+
for _, name := range reportedFeatures {
97+
parts = append(parts, name+" "+yesNo(caps.Has(name)))
98+
}
99+
if len(parts) == 0 {
100+
return "none"
101+
}
102+
return strings.Join(parts, ", ")
103+
}
104+
105+
// yesNo renders a capability the way the block reads it.
106+
func yesNo(v bool) string {
107+
if v {
108+
return "yes"
109+
}
110+
return "no"
111+
}
112+
113+
// reportUsage is the flag's one-line help, kept here next to what it prints.
114+
var reportUsage = fmt.Sprintf(
115+
"print the versions and machine facts a bug report needs, then exit "+
116+
"(no UI, no privileges, nothing about you or your server: paste it "+
117+
"into a %s issue)",
118+
toolName)

0 commit comments

Comments
 (0)