Skip to content

Commit eea0eec

Browse files
committed
Enhance README and implement job history feature in GitLab Runner TUI. Added job history view with recent job runs, status, and duration. Updated main application to support debug mode and improved configuration handling. Introduced HistoryView for displaying job history and integrated it into the main model.
1 parent d1d3521 commit eea0eec

File tree

5 files changed

+450
-19
lines changed

5 files changed

+450
-19
lines changed

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ A Terminal User Interface (TUI) for managing GitLab runners on Debian hosts. Thi
66

77
- **Runner Management**: View all configured GitLab runners with their status
88
- **Log Viewer**: Real-time log viewing with filtering and auto-scroll
9+
- **Job History**: View recent job runs with runner information, status, and duration
910
- **Configuration Editor**: Update runner concurrency, limits, and other settings
1011
- **System Monitor**: View service status, CPU/memory usage, and restart services
12+
- **Debug Mode**: Enable verbose logging for troubleshooting
1113
- **Keyboard Navigation**: Easy tab-based navigation between views
1214

1315
## Prerequisites
@@ -65,14 +67,28 @@ gitlab-runner-tui
6567
# Run with custom config path
6668
gitlab-runner-tui -config /path/to/config.toml
6769

70+
# Run in debug mode for verbose logging
71+
gitlab-runner-tui -debug
72+
73+
# Show help and default paths
74+
gitlab-runner-tui -help
75+
6876
# If running without sudo, it will check ~/.gitlab-runner/config.toml
6977
```
7078

79+
### Default Config Paths
80+
81+
The tool checks for configuration files in this order:
82+
1. `/etc/gitlab-runner/config.toml` (system-wide)
83+
2. `~/.gitlab-runner/config.toml` (user-specific)
84+
85+
You can override with the `-config` flag.
86+
7187
## Keyboard Shortcuts
7288

7389
### Global
7490
- `Tab` / `Shift+Tab`: Navigate between tabs
75-
- `1-4`: Jump to specific tab (Runners, Logs, Config, System)
91+
- `1-5`: Jump to specific tab (Runners, Logs, Config, System, History)
7692
- `q`: Quit (or go back from logs view)
7793
- `Ctrl+C`: Force quit
7894

@@ -99,6 +115,10 @@ gitlab-runner-tui -config /path/to/config.toml
99115
- `r`: Refresh system status
100116
- `s`: Restart GitLab Runner service
101117

118+
### History View
119+
- `r`: Refresh job history
120+
- `↑/↓`: Navigate job list
121+
102122
## Configuration
103123

104124
The tool reads and modifies the standard GitLab Runner configuration file (usually `/etc/gitlab-runner/config.toml`).

cmd/gitlab-runner-tui/main.go

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,26 @@ type model struct {
1919
logsView *ui.LogsView
2020
configView *ui.ConfigView
2121
systemView *ui.SystemView
22+
historyView *ui.HistoryView
2223
width int
2324
height int
2425
quitting bool
26+
debugMode bool
2527
}
2628

27-
func initialModel(configPath string) model {
29+
func initialModel(configPath string, debugMode bool) model {
2830
service := runner.NewService(configPath)
31+
service.SetDebugMode(debugMode)
2932

3033
return model{
31-
tabs: []string{"Runners", "Logs", "Config", "System"},
34+
tabs: []string{"Runners", "Logs", "Config", "System", "History"},
3235
activeTab: 0,
3336
runnersView: ui.NewRunnersView(service),
3437
logsView: ui.NewLogsView(service),
3538
configView: ui.NewConfigView(configPath),
3639
systemView: ui.NewSystemView(service),
40+
historyView: ui.NewHistoryView(service),
41+
debugMode: debugMode,
3742
}
3843
}
3944

@@ -43,6 +48,7 @@ func (m model) Init() tea.Cmd {
4348
m.logsView.Init(),
4449
m.configView.Init(),
4550
m.systemView.Init(),
51+
m.historyView.Init(),
4652
)
4753
}
4854

@@ -58,6 +64,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5864
m.logsView.Update(msg)
5965
m.configView.Update(msg)
6066
m.systemView.Update(msg)
67+
m.historyView.Update(msg)
6168

6269
return m, nil
6370

@@ -79,7 +86,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7986
m.activeTab = (m.activeTab - 1 + len(m.tabs)) % len(m.tabs)
8087
return m, nil
8188

82-
case "1", "2", "3", "4":
89+
case "1", "2", "3", "4", "5":
8390
if idx := int(msg.String()[0] - '1'); idx < len(m.tabs) {
8491
m.activeTab = idx
8592
}
@@ -112,6 +119,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
112119
updatedView, cmd := m.systemView.Update(msg)
113120
m.systemView = updatedView.(*ui.SystemView)
114121
cmds = append(cmds, cmd)
122+
case 4:
123+
updatedView, cmd := m.historyView.Update(msg)
124+
m.historyView = updatedView.(*ui.HistoryView)
125+
cmds = append(cmds, cmd)
115126
}
116127

117128
return m, tea.Batch(cmds...)
@@ -134,6 +145,8 @@ func (m model) View() string {
134145
content = m.configView.View()
135146
case 3:
136147
content = m.systemView.View()
148+
case 4:
149+
content = m.historyView.View()
137150
}
138151

139152
return lipgloss.JoinVertical(
@@ -159,18 +172,49 @@ func (m model) renderTabBar() string {
159172

160173
func main() {
161174
var configPath string
162-
flag.StringVar(&configPath, "config", "/etc/gitlab-runner/config.toml", "Path to GitLab Runner config file")
175+
var debugMode bool
176+
var showHelp bool
177+
178+
defaultConfig := "/etc/gitlab-runner/config.toml"
179+
180+
flag.StringVar(&configPath, "config", defaultConfig, "Path to GitLab Runner config file")
181+
flag.BoolVar(&debugMode, "debug", false, "Enable debug mode for verbose logging")
182+
flag.BoolVar(&showHelp, "help", false, "Show help information")
183+
flag.BoolVar(&showHelp, "h", false, "Show help information")
184+
185+
flag.Usage = func() {
186+
fmt.Fprintf(os.Stderr, "GitLab Runner TUI - Terminal User Interface for managing GitLab runners\n\n")
187+
fmt.Fprintf(os.Stderr, "Usage: %s [options]\n\n", os.Args[0])
188+
fmt.Fprintf(os.Stderr, "Options:\n")
189+
flag.PrintDefaults()
190+
fmt.Fprintf(os.Stderr, "\nDefault config paths checked:\n")
191+
fmt.Fprintf(os.Stderr, " 1. %s (system-wide)\n", defaultConfig)
192+
fmt.Fprintf(os.Stderr, " 2. $HOME/.gitlab-runner/config.toml (user-specific)\n")
193+
fmt.Fprintf(os.Stderr, "\nIf no config is found at the default path, the user-specific path is tried.\n")
194+
}
195+
163196
flag.Parse()
197+
198+
if showHelp {
199+
flag.Usage()
200+
os.Exit(0)
201+
}
164202

165-
if _, err := os.Stat(configPath); os.IsNotExist(err) {
166-
altPath := os.ExpandEnv("$HOME/.gitlab-runner/config.toml")
167-
if _, err := os.Stat(altPath); err == nil {
168-
configPath = altPath
203+
// Check if config exists at specified path
204+
if configPath == defaultConfig {
205+
if _, err := os.Stat(configPath); os.IsNotExist(err) {
206+
altPath := os.ExpandEnv("$HOME/.gitlab-runner/config.toml")
207+
if _, err := os.Stat(altPath); err == nil {
208+
configPath = altPath
209+
if debugMode {
210+
fmt.Printf("Using config from: %s\n", configPath)
211+
}
212+
}
169213
}
170214
}
171215

172216
p := tea.NewProgram(
173-
initialModel(configPath),
217+
initialModel(configPath, debugMode),
174218
tea.WithAltScreen(),
175219
tea.WithMouseCellMotion(),
176220
)

pkg/runner/service.go

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io"
77
"os/exec"
88
"regexp"
9+
"strconv"
910
"strings"
1011
"time"
1112
)
@@ -17,6 +18,8 @@ type Service interface {
1718
StreamRunnerLogs(name string) (io.ReadCloser, error)
1819
RestartRunner() error
1920
GetSystemStatus() (*SystemStatus, error)
21+
GetJobHistory(limit int) ([]Job, error)
22+
SetDebugMode(enabled bool)
2023
}
2124

2225
type SystemStatus struct {
@@ -30,6 +33,7 @@ type SystemStatus struct {
3033

3134
type gitlabRunnerService struct {
3235
configPath string
36+
debugMode bool
3337
}
3438

3539
func NewService(configPath string) Service {
@@ -126,7 +130,12 @@ func (s *gitlabRunnerService) GetRunnerStatus(name string) (*Runner, error) {
126130
}
127131

128132
func (s *gitlabRunnerService) GetRunnerLogs(name string, lines int) ([]string, error) {
129-
cmd := exec.Command("journalctl", "-u", "gitlab-runner", "-n", fmt.Sprintf("%d", lines), "--no-pager")
133+
args := []string{"-u", "gitlab-runner", "-n", fmt.Sprintf("%d", lines), "--no-pager"}
134+
if s.debugMode {
135+
args = append(args, "-o", "verbose")
136+
}
137+
138+
cmd := exec.Command("journalctl", args...)
130139
output, err := cmd.Output()
131140
if err != nil {
132141
cmd = exec.Command("tail", "-n", fmt.Sprintf("%d", lines), "/var/log/gitlab-runner.log")
@@ -241,4 +250,130 @@ func extractTimestamp(output string) string {
241250
return strings.TrimSpace(parts[1])
242251
}
243252
return ""
253+
}
254+
255+
func (s *gitlabRunnerService) GetJobHistory(limit int) ([]Job, error) {
256+
var jobs []Job
257+
258+
// Try to get job history from journalctl logs
259+
cmd := exec.Command("journalctl", "-u", "gitlab-runner", "-n", fmt.Sprintf("%d", limit*10), "--no-pager", "-r")
260+
output, err := cmd.Output()
261+
if err != nil {
262+
// Fallback to log file
263+
cmd = exec.Command("tail", "-n", fmt.Sprintf("%d", limit*10), "/var/log/gitlab-runner.log")
264+
output, err = cmd.Output()
265+
if err != nil {
266+
return nil, fmt.Errorf("failed to get job history: %w", err)
267+
}
268+
}
269+
270+
lines := strings.Split(string(output), "\n")
271+
jobMap := make(map[int]*Job)
272+
273+
for _, line := range lines {
274+
job := s.parseJobFromLog(line)
275+
if job != nil {
276+
if existing, ok := jobMap[job.ID]; ok {
277+
// Update existing job with new info
278+
if job.Status != "" {
279+
existing.Status = job.Status
280+
}
281+
if !job.Started.IsZero() {
282+
existing.Started = job.Started
283+
}
284+
if !job.Finished.IsZero() {
285+
existing.Finished = job.Finished
286+
existing.Duration = job.Finished.Sub(existing.Started)
287+
}
288+
if job.RunnerName != "" {
289+
existing.RunnerName = job.RunnerName
290+
}
291+
if job.ExitCode != 0 {
292+
existing.ExitCode = job.ExitCode
293+
}
294+
} else {
295+
jobMap[job.ID] = job
296+
}
297+
}
298+
299+
if len(jobMap) >= limit {
300+
break
301+
}
302+
}
303+
304+
// Convert map to slice
305+
for _, job := range jobMap {
306+
jobs = append(jobs, *job)
307+
}
308+
309+
// Sort by started time (newest first)
310+
for i := 0; i < len(jobs)-1; i++ {
311+
for j := i + 1; j < len(jobs); j++ {
312+
if jobs[i].Started.Before(jobs[j].Started) {
313+
jobs[i], jobs[j] = jobs[j], jobs[i]
314+
}
315+
}
316+
}
317+
318+
if len(jobs) > limit {
319+
jobs = jobs[:limit]
320+
}
321+
322+
return jobs, nil
323+
}
324+
325+
func (s *gitlabRunnerService) parseJobFromLog(line string) *Job {
326+
// Parse GitLab Runner log lines for job information
327+
jobStartRegex := regexp.MustCompile(`job=(\d+).*project=(\d+).*runner=([a-zA-Z0-9_-]+)`)
328+
jobStatusRegex := regexp.MustCompile(`job=(\d+).*status=(\w+)`)
329+
jobFinishRegex := regexp.MustCompile(`job=(\d+).*duration=([0-9.]+)s`)
330+
331+
job := &Job{}
332+
333+
// Check for job start
334+
if matches := jobStartRegex.FindStringSubmatch(line); len(matches) > 3 {
335+
jobID, _ := strconv.Atoi(matches[1])
336+
job.ID = jobID
337+
job.Project = matches[2]
338+
job.RunnerName = matches[3]
339+
job.Status = "running"
340+
341+
// Try to extract timestamp
342+
if idx := strings.Index(line, " "); idx > 0 {
343+
timeStr := line[:idx]
344+
if t, err := time.Parse("Jan 02 15:04:05", timeStr); err == nil {
345+
job.Started = t
346+
}
347+
}
348+
349+
return job
350+
}
351+
352+
// Check for job status update
353+
if matches := jobStatusRegex.FindStringSubmatch(line); len(matches) > 2 {
354+
jobID, _ := strconv.Atoi(matches[1])
355+
job.ID = jobID
356+
job.Status = matches[2]
357+
return job
358+
}
359+
360+
// Check for job completion
361+
if matches := jobFinishRegex.FindStringSubmatch(line); len(matches) > 2 {
362+
jobID, _ := strconv.Atoi(matches[1])
363+
job.ID = jobID
364+
job.Status = "completed"
365+
366+
if duration, err := strconv.ParseFloat(matches[2], 64); err == nil {
367+
job.Duration = time.Duration(duration * float64(time.Second))
368+
job.Finished = time.Now()
369+
}
370+
371+
return job
372+
}
373+
374+
return nil
375+
}
376+
377+
func (s *gitlabRunnerService) SetDebugMode(enabled bool) {
378+
s.debugMode = enabled
244379
}

pkg/runner/types.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,19 @@ type Runner struct {
2020
}
2121

2222
type Job struct {
23-
ID int
24-
Name string
25-
Status string
26-
Stage string
27-
Project string
28-
Pipeline int
29-
Started time.Time
30-
Duration time.Duration
23+
ID int
24+
Name string
25+
Status string
26+
Stage string
27+
Project string
28+
Pipeline int
29+
Started time.Time
30+
Finished time.Time
31+
Duration time.Duration
32+
RunnerName string
33+
RunnerID string
34+
ExitCode int
35+
URL string
3136
}
3237

3338
type Config struct {

0 commit comments

Comments
 (0)