Gitfed
bastien-mrq/gitfed/ Commits/ 06effe6

gitfed-ctl: surface every step's real error output, not just build's

Found live: a failing pull/import/apply/rollout step only ever showed a bare "exit status 1" — the actual git/kubectl/docker error message was captured but silently dropped since only the build stream's lines were kept. Also stop discarding kubectl exec's stderr in the backup step for the same reason.

bastien-mrq 2026-07-30 06:39 commit 06effe65acbc5133d893536bfcde5d82dd798a76 parent 0c2bd2100fe63443b9ee2b91dd8a377c229e76f9
5 files changed +56 −26
M CHANGELOG.md +4 −0
M cmd/gitfed-ctl/exec.go +15 −3
M cmd/gitfed-ctl/keys.go +1 −1
M cmd/gitfed-ctl/model.go +9 −6
M cmd/gitfed-ctl/screens_updates.go +27 −16
CHANGELOG.md
diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b20ff..313ee62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ A bullet starting with `**BREAKING:**` flags a change gitfed-ctl's update wizard makes you acknowledge individually before it will let you upgrade past that version. +## 1.2.13 + +- Fixed a real bug found live: `gitfed-ctl`'s update wizard only ever captured the "build" step's output, so any other step failing (pull, import, apply, rollout) showed a bare "Erreur : exit status 1" with no way to tell why. Every step's output is now captured and shown when it's the one running or the one that failed. Also stopped discarding `kubectl exec`'s stderr in the backup step for the same reason. + ## 1.2.12 - Server-side syntax highlighting for file views, using [chroma](https://github.com/alecthomas/chroma) (Go, no client-side JS) — matches the approach ROADMAP.md already called out given gitfed's strict CSP (a single hashed inline script; an external JS highlighter would either break that or have to be folded into the hashed blob). Highlighted output uses inline per-token styles, already allowed by the existing `style-src 'unsafe-inline'`, so no CSP change was needed. Falls back to the previous plain `<pre>` rendering for any file chroma doesn't recognize a language for.
cmd/gitfed-ctl/exec.go
diff --git a/cmd/gitfed-ctl/exec.go b/cmd/gitfed-ctl/exec.go index 81f8eec..430a39e 100644 --- a/cmd/gitfed-ctl/exec.go +++ b/cmd/gitfed-ctl/exec.go @@ -2,9 +2,12 @@ package main import ( "bufio" + "bytes" "context" + "fmt" "io" "os/exec" + "strings" tea "github.com/charmbracelet/bubbletea" ) @@ -81,10 +84,19 @@ func runQuiet(ctx context.Context, dir, name string, args ...string) (string, er // runToFile runs name(args...) and streams its raw stdout straight into w // — used for the one step whose output is binary (backup.go's `tar` pull), // where treating it as line-oriented text like runStreamed would corrupt -// it. stderr is discarded rather than mixed into w, since w is expected to -// be exactly the command's stdout bytes. +// it. stderr is captured separately (not mixed into w, since w is expected +// to be exactly the command's stdout bytes) and folded into the returned +// error so a failure here isn't just a bare "exit status 1" either. func runToFile(ctx context.Context, w io.Writer, name string, args ...string) error { cmd := exec.CommandContext(ctx, name, args...) cmd.Stdout = w - return cmd.Run() + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + if stderr.Len() > 0 { + return fmt.Errorf("%w: %s", err, strings.TrimSpace(stderr.String())) + } + return err + } + return nil }
cmd/gitfed-ctl/keys.go
diff --git a/cmd/gitfed-ctl/keys.go b/cmd/gitfed-ctl/keys.go index b16b650..235402b 100644 --- a/cmd/gitfed-ctl/keys.go +++ b/cmd/gitfed-ctl/keys.go @@ -124,7 +124,7 @@ func (m model) handleConfirmKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.importStep = pipelineStepStatus{} m.applyStep = pipelineStepStatus{} m.rolloutStep = pipelineStepStatus{} - m.buildLines = nil + m.stepLogs = make(map[string][]string) m.pipelineErr = nil return m, tea.Batch(m.cmdRunPipeline(), listen(m.msgCh)) }
cmd/gitfed-ctl/model.go
diff --git a/cmd/gitfed-ctl/model.go b/cmd/gitfed-ctl/model.go index 82384c8..f806423 100644 --- a/cmd/gitfed-ctl/model.go +++ b/cmd/gitfed-ctl/model.go @@ -70,9 +70,13 @@ type model struct { // updates: running backupStep, pullStep, buildStep, importStep, applyStep, rolloutStep pipelineStepStatus - buildLines []string - resolvedVersion string - pipelineErr error + // stepLogs captures every step's output, keyed by stream name (see + // exec.go's logLineMsg) — not just the build step's, so a failing + // "pull"/"apply"/"rollout" step can show its real error output too + // instead of just the bare "exit status 1" a step's Go error carries. + stepLogs map[string][]string + resolvedVersion string + pipelineErr error spin spinner.Model } @@ -91,6 +95,7 @@ func initialModel(sourceDir string) model { cancel: cancel, sourceDir: sourceDir, backupToggle: true, + stepLogs: make(map[string][]string), spin: sp, } } @@ -162,9 +167,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, listen(m.msgCh) case logLineMsg: - if msg.stream == "build" { - m.buildLines = append(m.buildLines, msg.line) - } + m.stepLogs[msg.stream] = append(m.stepLogs[msg.stream], msg.line) return m, listen(m.msgCh) case versionResolvedMsg:
cmd/gitfed-ctl/screens_updates.go
diff --git a/cmd/gitfed-ctl/screens_updates.go b/cmd/gitfed-ctl/screens_updates.go index ee154ff..d39487e 100644 --- a/cmd/gitfed-ctl/screens_updates.go +++ b/cmd/gitfed-ctl/screens_updates.go @@ -83,28 +83,30 @@ func (m model) viewRunning() string { var b strings.Builder type namedStep struct { - label string - s pipelineStepStatus + label string + stream string + s pipelineStepStatus } var steps []namedStep if m.backupToggle { - steps = append(steps, namedStep{"Sauvegarde", m.backupStep}) + steps = append(steps, namedStep{"Sauvegarde", "backup", m.backupStep}) } steps = append(steps, - namedStep{"Récupération du code", m.pullStep}, - namedStep{"Build de l'image", m.buildStep}, - namedStep{"Import dans containerd", m.importStep}, - namedStep{"Application du déploiement", m.applyStep}, - namedStep{"Déploiement du pod", m.rolloutStep}, + namedStep{"Récupération du code", "pull", m.pullStep}, + namedStep{"Build de l'image", "build", m.buildStep}, + namedStep{"Import dans containerd", "import", m.importStep}, + namedStep{"Application du déploiement", "apply", m.applyStep}, + namedStep{"Déploiement du pod", "rollout", m.rolloutStep}, ) current := -1 - if m.pipelineErr == nil { - for i, st := range steps { - if !st.s.Done && st.s.Err == nil { - current = i - break - } + failed := -1 + for i, st := range steps { + if st.s.Err != nil { + failed = i + } + if current == -1 && m.pipelineErr == nil && !st.s.Done && st.s.Err == nil { + current = i } } @@ -125,8 +127,17 @@ func (m model) viewRunning() string { b.WriteString("\n" + styleDanger.Render("Erreur : "+m.pipelineErr.Error())) } - if len(m.buildLines) > 0 && !m.buildStep.Done && m.buildStep.Err == nil { - b.WriteString("\n\n" + styleLog.Render(tailLines(m.buildLines, 10))) + // Show the live/failed step's own captured output — this is what + // actually explains a bare "exit status 1" (e.g. git's real refusal + // reason, docker's real build failure, kubectl's real rejection). + logStep := current + if failed != -1 { + logStep = failed + } + if logStep != -1 { + if lines := m.stepLogs[steps[logStep].stream]; len(lines) > 0 { + b.WriteString("\n\n" + styleLog.Render(tailLines(lines, 10))) + } } return stylePanel.Render(b.String())