Skip to content

Commit

Permalink
refactor: cleans up code
Browse files Browse the repository at this point in the history
  • Loading branch information
jmgilman committed Sep 15, 2024
1 parent bf1e26a commit 35af749
Show file tree
Hide file tree
Showing 9 changed files with 401 additions and 396 deletions.
4 changes: 2 additions & 2 deletions forge/cli/cmd/cmds/ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package cmds
import (
"log/slog"

"github.com/input-output-hk/catalyst-forge/forge/cli/tui/pipeline"
"github.com/input-output-hk/catalyst-forge/forge/cli/tui/ci"
)

type CICmd struct {
Expand All @@ -19,5 +19,5 @@ func (c *CICmd) Run(logger *slog.Logger, global GlobalArgs) error {
}
opts := generateOpts(&flags, &global)
filters := []string{"^check.*$", "^build.*$", "^test.*$"}
return pipeline.Start(c.Path, filters, global.Local, opts...)
return ci.Run(c.Path, filters, global.Local, opts...)
}
4 changes: 4 additions & 0 deletions forge/cli/pkg/executor/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ func WithRedirect() LocalExecutorOption {

// NewLocalExecutor creates a new LocalExecutor with the given options.
func NewLocalExecutor(logger *slog.Logger, options ...LocalExecutorOption) *LocalExecutor {
if logger == nil {
logger = slog.New(slog.NewTextHandler(io.Discard, nil))
}

e := &LocalExecutor{
logger: logger,
}
Expand Down
110 changes: 110 additions & 0 deletions forge/cli/tui/ci/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package ci

import (
"log/slog"

"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/input-output-hk/catalyst-forge/forge/cli/pkg/earthly"
"github.com/input-output-hk/catalyst-forge/forge/cli/pkg/project"
"github.com/input-output-hk/catalyst-forge/forge/cli/tui"
)

// App represents the TUI application.
type App struct {
ci CI
logger *slog.Logger
window tui.Window
}

func (a App) Init() tea.Cmd {
a.logger.Info("Starting CI simulation")
return a.ci.Run()
}

func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
a.window.Resize(msg)
return a, nil
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc":
return a, tea.Quit
}
case spinner.TickMsg:
return a, tea.Batch(a.ci.UpdateSpinners(msg)...)
case CIRunFinishedMsg:
a.logger.Info("Received CI run finished message", "project", msg.Run.Project.Path, "target", msg.Run.Target)
if a.ci.Finished() {
a.logger.Info("All CI runs finished for current group")
out := a.ci.View()
cmd, err := a.ci.Next()
if err != nil {
a.logger.Info("No more runs")
return a, tea.Sequence(
tea.Println(out),
tea.Quit,
)
}

a.logger.Info("Starting next group")
return a, tea.Sequence(
tea.Println(out),
cmd,
)
}
}

return a, nil
}

func (a App) View() string {
return a.ci.View()
}

// Run starts the TUI application.
func Run(scanPath string,
filters []string,
local bool,
opts ...earthly.EarthlyExecutorOption,
) error {
logger, f, err := tui.NewLogger()
if err != nil {
return err
}
defer f.Close()

loader := project.NewDefaultProjectLoader(
false,
local,
project.GetDefaultRuntimes(logger),
logger,
)

ci := CI{
filters: filters,
loader: &loader,
logger: logger,
options: opts,
scanPath: scanPath,
}

logger.Info("Loading project")
if err := ci.Load(); err != nil {
return err
}

app := App{
ci: ci,
logger: logger,
}

logger.Info("Starting program")
p := tea.NewProgram(app)
if _, err := p.Run(); err != nil {
return err
}

return nil
}
244 changes: 244 additions & 0 deletions forge/cli/tui/ci/ci.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package ci

import (
"errors"
"fmt"
"log/slog"
"regexp"
"strings"

"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/input-output-hk/catalyst-forge/forge/cli/pkg/earthly"
"github.com/input-output-hk/catalyst-forge/forge/cli/pkg/executor"
"github.com/input-output-hk/catalyst-forge/forge/cli/pkg/project"
"github.com/input-output-hk/catalyst-forge/forge/cli/pkg/scan"
"github.com/input-output-hk/catalyst-forge/forge/cli/pkg/secrets"
"github.com/input-output-hk/catalyst-forge/tools/pkg/walker"
)

const (
RunStatusIdle RunStatus = "idle"
RunStatusRunning RunStatus = "running"
RunStatusFailed RunStatus = "failed"
RunStatusSuccess RunStatus = "success"
)

var (
checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓")
ErrNoMoreRuns = errors.New("no more runs")
)

// RunStatus represents the status of a CI run.
type RunStatus string

type CIRunFinishedMsg struct {
Run *CIRun
}

// CI represents a CI simulation.
type CI struct {
filters []string
groups []*CIRunGroup
index int
loader project.ProjectLoader
logger *slog.Logger
options []earthly.EarthlyExecutorOption
scanPath string
}

// Finished returns true if the active run group has finished.
func (c *CI) Finished() bool {
return c.groups[c.index].Finished()
}

// Load loads the CI runs to be executed.
func (c *CI) Load() error {
w := walker.NewDefaultFSWalker(nil)
var groups []*CIRunGroup

projects, err := scan.ScanProjects(c.scanPath, c.loader, &w, c.logger)
if err != nil {
return err
}

for _, filter := range c.filters {
var runs []*CIRun

filterExpr, err := regexp.Compile(filter)
if err != nil {
return err
}

for _, project := range projects {
if project.Earthfile != nil {
targets := project.Earthfile.FilterTargets(func(target string) bool {
return filterExpr.MatchString(target)
})

for _, target := range targets {
runs = append(runs, &CIRun{
Project: &project,
Status: RunStatusIdle,
Target: target,
logger: c.logger,
options: c.options,
spinner: spinner.New(),
})
}
}
}

groups = append(groups, &CIRunGroup{
Runs: runs,
})
}

c.groups = groups
return nil
}

// Next returns the next command to be executed. If there are no more runs, it
// returns an error.
func (c *CI) Next() (tea.Cmd, error) {
c.index++
if c.index >= len(c.groups) {
return nil, ErrNoMoreRuns
}

return c.groups[c.index].Run(), nil
}

// Run starts the CI simulation.
func (c *CI) Run() tea.Cmd {
var cmds []tea.Cmd
cmds = append(cmds, c.groups[0].Run())
for _, group := range c.groups {
for _, run := range group.Runs {
cmds = append(cmds, run.spinner.Tick)
}
}
return tea.Batch(cmds...)
}

// UpdateSpinner updates the spinners of the CI simulation.
func (c *CI) UpdateSpinners(msg tea.Msg) []tea.Cmd {
var cmds []tea.Cmd
for _, group := range c.groups {
for _, run := range group.Runs {
cmds = append(cmds, run.UpdateSpinner(msg))
}
}

return cmds
}

// View returns the current view of the CI simulation.
func (c *CI) View() string {
if len(c.groups) > 0 && c.index < len(c.groups) {
return c.groups[c.index].View()
} else {
return ""
}
}

// CIRunGroup represents a group of CI runs.
type CIRunGroup struct {
Runs []*CIRun
}

// Run starts the CI run group.
func (c *CIRunGroup) Run() tea.Cmd {
var cmds []tea.Cmd
for _, run := range c.Runs {
cmds = append(cmds, run.Run)
}

return tea.Batch(cmds...)
}

// View returns the view of the CI run group.
func (c *CIRunGroup) View() string {
var view string
for _, run := range c.Runs {
view += run.View() + "\n"
}

return strings.TrimSuffix(view, "\n")
}

// Finished returns true if all runs in the group have finished.
func (c *CIRunGroup) Finished() bool {
for _, run := range c.Runs {
if run.Status == RunStatusIdle || run.Status == RunStatusRunning {
return false
}
}

return true
}

// CIRun represents a CI run.
type CIRun struct {
Project *project.Project
Status RunStatus
Target string
logger *slog.Logger
options []earthly.EarthlyExecutorOption
spinner spinner.Model
}

// Run starts the CI run.
func (c *CIRun) Run() tea.Msg {
c.logger.Info("Running target", "project", c.Project.Path, "target", c.Target)
c.Status = RunStatusRunning
_, err := c.Project.RunTarget(
c.Target,
executor.NewLocalExecutor(c.logger),
secrets.NewDefaultSecretStore(),
c.options...,
)

if err != nil {
c.logger.Error("Failed to run target", "project", c.Project.Path, "target", c.Target, "error", err)
c.Status = RunStatusFailed
} else {
c.logger.Info("Target ran successfully", "project", c.Project.Path, "target", c.Target)
c.Status = RunStatusSuccess
}

return CIRunFinishedMsg{
Run: c,
}
}

// UpdateSpinner updates the spinner of the CI run.
func (c *CIRun) UpdateSpinner(msg tea.Msg) tea.Cmd {
switch msg := msg.(type) {
case spinner.TickMsg:
if msg.ID == c.spinner.ID() {
var cmd tea.Cmd
c.spinner, cmd = c.spinner.Update(msg)
return cmd
}
}

return nil
}

// View returns the view of the CI run.
func (c *CIRun) View() string {
switch c.Status {
case RunStatusIdle:
return fmt.Sprintf("%s %s", lipgloss.NewStyle().Foreground(lipgloss.Color("241")).SetString("•"), c.Project.Path+"+"+c.Target)
case RunStatusRunning:
return fmt.Sprintf("%s %s", c.spinner.View(), c.Project.Path+"+"+c.Target)
case RunStatusFailed:
return fmt.Sprintf("%s %s", lipgloss.NewStyle().Foreground(lipgloss.Color("196")).SetString("✗"), c.Project.Path+"+"+c.Target)
case RunStatusSuccess:
return fmt.Sprintf("%s %s", checkMark, c.Project.Path+"+"+c.Target)
default:
return ""
}
}
Loading

0 comments on commit 35af749

Please sign in to comment.