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