Skip to content

Commit

Permalink
feat: add workflow loader preflight task
Browse files Browse the repository at this point in the history
  • Loading branch information
aweris committed Aug 24, 2023
1 parent f9ad84b commit d4192f8
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 13 deletions.
7 changes: 6 additions & 1 deletion cmd/gale/validate/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,14 @@ func NewCommand() *cobra.Command {
return config.Client().Close()
},
RunE: func(cmd *cobra.Command, args []string) error {
validator := preflight.NewValidator(preflight.NewConsoleReporter())
validator := preflight.NewValidator(cmd.Context(), preflight.NewConsoleReporter())

validator.Register(preflight.StandardTasks()...)

// set the workflow and job names
opts.Workflow = args[0]
opts.Job = args[1]

return validator.Validate(opts)
},
}
Expand All @@ -52,6 +56,7 @@ func NewCommand() *cobra.Command {
cmd.Flags().StringVar(&opts.Repo, "repo", "", "owner/repo to load workflows from. If empty, repository information of the current directory will be used.")
cmd.Flags().StringVar(&opts.Branch, "branch", "", "branch to load workflows from. Only one of branch or tag can be used. Precedence is as follows: tag, branch.")
cmd.Flags().StringVar(&opts.Tag, "tag", "", "tag to load workflows from. Only one of branch or tag can be used. Precedence is as follows: tag, branch.")
cmd.Flags().StringVar(&opts.WorkflowsDir, "workflows-dir", "", "directory to load workflows from. If empty, workflows will be loaded from the default directory.")

return cmd
}
9 changes: 6 additions & 3 deletions pkg/preflight/preflight.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package preflight

import "fmt"
import (
"context"
"fmt"
)

// Validator entrypoint for preflight checks.
type Validator struct {
Expand All @@ -10,10 +13,10 @@ type Validator struct {
}

// NewValidator creates a new Validator.
func NewValidator(reporter Reporter) *Validator {
func NewValidator(ctx context.Context, reporter Reporter) *Validator {
return &Validator{
tasks: make(map[string]Task),
ctx: &Context{},
ctx: &Context{Context: ctx},
reporter: reporter,
}
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/preflight/preflight_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package preflight_test

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -37,7 +38,7 @@ func (m *MockReporter) Report(t preflight.Task, _ preflight.Result) error {
func TestValidator(t *testing.T) {
// Create the validator with a mock reporter
reporter := &MockReporter{}
validator := preflight.NewValidator(reporter)
validator := preflight.NewValidator(context.Background(), reporter)

// Register setups and checks
setupA := &MockTask{name: "setupA"}
Expand Down
2 changes: 2 additions & 0 deletions pkg/preflight/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ func (r *ConsoleReporter) Report(t Task, result Result) error {
log.Warn(msg.Content)
case Info:
log.Info(msg.Content)
case Debug:
log.Debug(msg.Content)
}
}

Expand Down
8 changes: 5 additions & 3 deletions pkg/preflight/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ type Task interface {

// Task names to make it easier to reference them in dependencies.
const (
NameDaggerCheck = "Dagger"
NameGHCheck = "GitHub CLI"
NameRepoLoader = "Repo"
NameDaggerCheck = "Dagger"
NameGHCheck = "GitHub CLI"
NameRepoLoader = "Repo"
NameWorkflowLoader = "Workflow"
)

// StandardTasks returns the standard tasks that are used in preflight checks.
Expand All @@ -29,5 +30,6 @@ func StandardTasks() []Task {
new(DaggerCheck),
new(GHCheck),
new(RepoLoader),
new(WorkflowLoader),
}
}
174 changes: 174 additions & 0 deletions pkg/preflight/task_workflows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package preflight

import (
"fmt"
"os"
"strings"

"github.com/aweris/gale/internal/config"
"github.com/aweris/gale/internal/core"
)

var _ Task = new(WorkflowLoader)

type WorkflowLoader struct{}

func (c *WorkflowLoader) Name() string {
return NameWorkflowLoader
}

func (c *WorkflowLoader) Type() TaskType {
return TaskTypeLoad
}

func (c *WorkflowLoader) DependsOn() []string {
return []string{NameDaggerCheck, NameRepoLoader}
}

func (c *WorkflowLoader) Run(ctx *Context, opts Options) Result {
var msg []Message

repo := ctx.Repo

workflows, err := repo.LoadWorkflows(ctx.Context, core.RepositoryLoadWorkflowOpts{WorkflowsDir: opts.WorkflowsDir})
if err != nil {
return Result{
Status: Failed,
Messages: []Message{
{Level: Error, Content: fmt.Sprintf("Load workflows failed: %s", err.Error())},
},
}
}

ctx.Workflows = workflows

if opts.WorkflowsDir == "" {
msg = append(msg, Message{Level: Info, Content: "Workflows are loaded from .github/workflows"})
} else {
msg = append(msg, Message{Level: Info, Content: fmt.Sprintf("Workflows are loaded from %s", opts.WorkflowsDir)})
}

workflow, ok := workflows[opts.Workflow]
if !ok {
msg = append(msg, Message{Level: Error, Content: fmt.Sprintf("Workflow %s is not found", opts.Workflow)})

return Result{Status: Failed, Messages: msg}
}

ctx.Workflow = workflow

msg = append(msg, Message{Level: Info, Content: fmt.Sprintf("Workflow %s is loaded", opts.Workflow)})

job, ok := workflow.Jobs[opts.Job]
if !ok {
msg = append(msg, Message{Level: Error, Content: fmt.Sprintf("Job %s is not found", opts.Job)})

return Result{Status: Failed, Messages: msg}
}

ctx.Job = &job

msg = append(msg, Message{Level: Info, Content: fmt.Sprintf("Job %s is loaded", opts.Job)})

// TODO: find a better way to do this. Maybe pass target directory as an option or add clean up function to the context and call it at the end.
dir, err := os.MkdirTemp("", "ghx-home")
if err != nil {
msg = append(msg, Message{Level: Error, Content: fmt.Sprintf("Failed to create temporary GHX home directory: %s", err.Error())})

return Result{Status: Failed, Messages: msg}
}

config.SetGhxHome(dir)

for _, step := range job.Steps {
// check if the step is an action
switch step.Type() {
case core.StepTypeAction:
if ctx.CustomActions == nil {
ctx.CustomActions = make(map[string]*core.CustomAction)
}

// check if the action is already loaded
if ctx.CustomActions[step.Uses] != nil {
continue
}

// load the action
ca, err := core.LoadActionFromSource(ctx.Context, step.Uses)
if err != nil {
msg = append(msg, Message{Level: Error, Content: fmt.Sprintf("Load action %s failed: %s", step.Uses, err.Error())})

return Result{Status: Failed, Messages: msg}
}

// add the action to the list
ctx.CustomActions[step.Uses] = ca

msg = append(msg, Message{Level: Debug, Content: fmt.Sprintf("Action %s is used in the workflow", step.Uses)})

// check if the action uses a Docker image
if ca.Meta.Runs.Image != "" && strings.HasPrefix(ca.Meta.Runs.Image, "docker://") {
// trim docker:// prefix
image := strings.TrimPrefix(ca.Meta.Runs.Image, "docker://")

// check if map is initialized
if ctx.DockerImages == nil {
ctx.DockerImages = make(map[string]bool)
}

// check if the image is already used
if ctx.DockerImages[image] {
continue
}

// add the image to the list
ctx.DockerImages[image] = true

msg = append(msg, Message{Level: Debug, Content: fmt.Sprintf("Docker image %s is used in the workflow", image)})
}
case core.StepTypeDocker:
// trim docker:// prefix
image := strings.TrimPrefix(step.Uses, "docker://")

// check if map is initialized
if ctx.DockerImages == nil {
ctx.DockerImages = make(map[string]bool)
}

// check if the image is already used
if ctx.DockerImages[image] {
continue
}

// add the image to the list
ctx.DockerImages[image] = true

msg = append(msg, Message{Level: Debug, Content: fmt.Sprintf("Docker image %s is used in the workflow", image)})
case core.StepTypeRun:
// check if map is initialized
if ctx.Shells == nil {
ctx.Shells = make(map[string]bool)
}

// empty shell means the default shell. skip it.
if step.Shell == "" {
continue
}

// check if the shell is already used
if ctx.Shells[step.Shell] {
continue
}

// add the shell to the list

ctx.Shells[step.Shell] = true

msg = append(msg, Message{Level: Debug, Content: fmt.Sprintf("Shell %s is used in the workflow", step.Shell)})
default:
msg = append(msg, Message{Level: Warning, Content: fmt.Sprintf("Step %s is not supported", step.Type())})
}
}

return Result{Status: Passed, Messages: msg}
}
20 changes: 15 additions & 5 deletions pkg/preflight/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,31 @@ import (

// Options represents the options for the preflight checks.
type Options struct {
Repo string // Repo is the name of the repository. It should be in the format of owner/repo.
Branch string // Branch is the name of the branch.
Tag string // Tag is the name of the tag.
Repo string // Repo is the name of the repository. It should be in the format of owner/repo.
Branch string // Branch is the name of the branch.
Tag string // Tag is the name of the tag.
WorkflowsDir string // WorkflowsDir is the directory that contains the workflows.
Workflow string // Workflow is the name of the workflow.
Job string // Job is the name of the job
}

// Context represents the context of the preflight checks.
type Context struct {
Context context.Context // Context is the context of the preflight checks.
Repo *core.Repository // Repo represents a GitHub repository that is used for the preflight checks.
Context context.Context // Context is the context of the preflight checks.
Repo *core.Repository // Repo represents a GitHub repository that is used for the preflight checks.
Workflows map[string]*core.Workflow // Workflows is the list of workflows that are loaded.
Workflow *core.Workflow // Workflow is the workflow that is used for the preflight checks.
Job *core.Job // Job is the job that is used for the preflight checks.
CustomActions map[string]*core.CustomAction // CustomActions is the list of custom actions that are loaded.
Shells map[string]bool // Shells is the list of shells that are used in the workflow.
DockerImages map[string]bool // DockerImages is the list of Docker images that are used in the workflow.
}

// MessageLevel is the level of the message. It can be INFO, WARNING, or ERROR.
type MessageLevel string

const (
Debug MessageLevel = "DEBUG"
Info MessageLevel = "INFO"
Warning MessageLevel = "WARNING"
Error MessageLevel = "ERROR"
Expand Down

0 comments on commit d4192f8

Please sign in to comment.