Skip to content

Commit

Permalink
feat: add basic execution reports
Browse files Browse the repository at this point in the history
  • Loading branch information
aweris committed Oct 17, 2023
1 parent fdd3a23 commit 391478b
Show file tree
Hide file tree
Showing 15 changed files with 391 additions and 124 deletions.
8 changes: 6 additions & 2 deletions ghx/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package context

import (
"context"
"path/filepath"

"dagger.io/dagger"

Expand Down Expand Up @@ -89,7 +88,12 @@ func New(std context.Context, client *dagger.Client) (*Context, error) {
}

// set secrets ctx
ctx.Secrets.MountPath = filepath.Join(ctx.GhxConfig.HomeDir, "secrets", "secret.json")
secretsMountPath, err := ctx.GetSecretsPath()

ctx.Secrets.MountPath = secretsMountPath
if err != nil {
return nil, err
}

if err := fs.EnsureFile(ctx.Secrets.MountPath); err != nil {
return nil, err
Expand Down
82 changes: 63 additions & 19 deletions ghx/context/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/aweris/gale/ghx/core"
"github.com/aweris/gale/internal/fs"
"github.com/aweris/gale/internal/log"
)

// SetWorkflow creates a new execution context with the given workflow and sets it to the context.
Expand Down Expand Up @@ -35,6 +36,17 @@ func (c *Context) SetWorkflow(wr *core.WorkflowRun) error {
return nil
}

func (c *Context) UnsetWorkflow(result RunResult) {
// ignoring error since directory must exist at this point of execution
dir, _ := c.GetWorkflowRunPath()

report := NewWorkflowRunReport(&result, c.Execution.WorkflowRun)

if err := fs.WriteJSONFile(filepath.Join(dir, "workflow_run.json"), report); err != nil {
log.Errorf("failed to write workflow run", "error", err, "workflow", c.Execution.WorkflowRun.Workflow.Name)
}
}

// SetJob sets the given job to the execution context.
func (c *Context) SetJob(jr *core.JobRun) error {
if c.Execution.WorkflowRun == nil {
Expand Down Expand Up @@ -78,7 +90,7 @@ func (c *Context) SetJob(jr *core.JobRun) error {
}

// UnsetJob unsets the job from the execution context.
func (c *Context) UnsetJob() {
func (c *Context) UnsetJob(result RunResult) {
jr := c.Execution.JobRun

// update the job run in the workflow run
Expand All @@ -88,10 +100,6 @@ func (c *Context) UnsetJob() {
if c.Execution.WorkflowRun.Conclusion == core.ConclusionSuccess && jr.Conclusion != core.ConclusionSuccess {
c.Execution.WorkflowRun.Conclusion = jr.Conclusion
}

// unset the job run from the execution context
c.Execution.JobRun = nil

// unset the job run from the github context
c.Github.Job = ""

Expand All @@ -100,21 +108,19 @@ func (c *Context) UnsetJob() {

// reset matrix context
c.Matrix = make(MatrixContext)
}

// GetJobRunPath returns the path of the current job run path. If the path does not exist, it creates it.
func (c *Context) GetJobRunPath() (string, error) {
if c.Execution.JobRun == nil {
return "", errors.New("no job is set")
}
// write the job run result to the file system
// ignoring error since directory must be exist at this point of execution
dir, _ := c.GetJobRunPath()

path := filepath.Join(c.GhxConfig.HomeDir, "runs", c.Execution.WorkflowRun.RunID, "jobs", c.Execution.JobRun.RunID)
report := NewJobRunReport(&result, c.Execution.JobRun)

if err := fs.EnsureDir(path); err != nil {
return "", err
if err := fs.WriteJSONFile(filepath.Join(dir, "job_run.json"), report); err != nil {
log.Errorf("failed to write job run", "error", err, "workflow", c.Execution.WorkflowRun.Workflow.Name)
}

return path, nil
// unset the job run from the execution context
c.Execution.JobRun = nil
}

// SetJobResults sets the status of the job.
Expand Down Expand Up @@ -151,9 +157,9 @@ func (c *Context) SetStep(sr *core.StepRun) error {
}

// UnsetStep unsets the step from the execution context.
func (c *Context) UnsetStep() error {
func (c *Context) UnsetStep(result RunResult) {
if c.Execution.StepRun == nil {
return errors.New("no step is set")
return
}

// TODO: improve this logic
Expand Down Expand Up @@ -185,9 +191,26 @@ func (c *Context) UnsetStep() error {

c.Steps[sr.Step.ID] = sc

c.Execution.StepRun = nil
// only export the result of the main stage
if c.Execution.StepRun.Stage == core.StepStageMain {
// write the job run result to the file system
// ignoring error since directory must exist at this point of execution
dir, _ := c.GetStepRunPath()

return nil
report := NewStepRunReport(&result, c.Execution.StepRun)

if err := fs.WriteJSONFile(filepath.Join(dir, "step_run.json"), &report); err != nil {
log.Errorf("failed to write step run", "error", err, "workflow", c.Execution.WorkflowRun.Workflow.Name)
}

if c.Execution.StepRun.Summary != "" {
if err := fs.WriteFile(filepath.Join(dir, "summary.md"), []byte(c.Execution.StepRun.Summary), 0600); err != nil {
log.Errorf("failed to write step run summary", "error", err, "workflow", c.Execution.WorkflowRun.Workflow.Name)
}
}
}

c.Execution.StepRun = nil
}

func (c *Context) SetStepResults(conclusion, outcome core.Conclusion) error {
Expand Down Expand Up @@ -234,6 +257,27 @@ func (c *Context) SetStepState(key, value string) error {
return nil
}

// AddStepPath adds the given path to the step path.
func (c *Context) AddStepPath(path string) error {
if c.Execution.StepRun == nil {
return errors.New("no step is set")
}

c.Execution.StepRun.Path = append(c.Execution.StepRun.Path, path)

return nil
}

func (c *Context) SetStepEnv(key, value string) error {
if c.Execution.StepRun == nil {
return errors.New("no step is set")
}

c.Execution.StepRun.Environment[key] = value

return nil
}

func (c *Context) SetAction(action *core.CustomAction) {
c.Execution.CurrentAction = action
}
Expand Down
85 changes: 85 additions & 0 deletions ghx/context/paths.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package context

import (
"errors"
"os"
"path/filepath"

"github.com/aweris/gale/internal/fs"
)

// GetMetadataPath returns the path of the metadata path. If the path does not exist, it creates it. This path assumes
// zenith module set a cache for metadata, otherwise it will be empty every time it runs.
func (c *Context) GetMetadataPath() (string, error) {
return EnsureDir(c.GhxConfig.HomeDir, "metadata")
}

// GetActionsPath returns the path of the custom action repositories clones and stores. If the path does not exist, it
// creates it.
func (c *Context) GetActionsPath() (string, error) {
return EnsureDir(c.GhxConfig.HomeDir, "actions")
}

// GetSecretsPath returns the path of the secrets.json file containing the secrets. If the path does not exist, it
// creates it.
func (c *Context) GetSecretsPath() (string, error) {
file := filepath.Join(c.GhxConfig.HomeDir, "secrets", "secrets.json")

// ensure file and directory exists
if err := fs.EnsureFile(file); err != nil {
return "", err
}

// if file content is empty, write empty json object to avoid json unmarshal error
stat, err := os.Stat(file)
if err != nil {
return "", err
}

if stat.Size() == 0 {
fs.WriteJSONFile(file, map[string]string{})
}

return file, nil
}

// GetWorkflowRunPath returns the path of the current workflow run path. If the path does not exist, it creates it. If
// the workflow run is not set, it returns an error.
func (c *Context) GetWorkflowRunPath() (string, error) {
if c.Execution.WorkflowRun == nil {
return "", errors.New("no workflow run is set")
}

return EnsureDir(c.GhxConfig.HomeDir, "runs", c.Execution.WorkflowRun.RunID)
}

// GetJobRunPath returns the path of the current job run path. If the path does not exist, it creates it. If the job run
// is not set, it returns an error.
func (c *Context) GetJobRunPath() (string, error) {
if c.Execution.JobRun == nil {
return "", errors.New("no job is set")
}

return EnsureDir(c.GhxConfig.HomeDir, "runs", c.Execution.WorkflowRun.RunID, "jobs", c.Execution.JobRun.RunID)
}

// GetStepRunPath returns the path of the current step run path. If the path does not exist, it creates it. If the step
// run is not set, it returns an error.
func (c *Context) GetStepRunPath() (string, error) {
if c.Execution.StepRun == nil {
return "", errors.New("no step is set")
}

return EnsureDir(c.GhxConfig.HomeDir, "runs", c.Execution.WorkflowRun.RunID, "jobs", c.Execution.JobRun.RunID, "steps", c.Execution.StepRun.Step.ID)
}

// EnsureDir return the joined path and ensures that the directory exists. and returns the joined path.
func EnsureDir(path ...string) (string, error) {
joined := filepath.Join(path...)

if err := fs.EnsureDir(joined); err != nil {
return "", err
}

return joined, nil
}
124 changes: 124 additions & 0 deletions ghx/context/reports.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package context

import (
"time"

"github.com/aweris/gale/ghx/core"
)

// RunResult is the result of the task execution
type RunResult struct {
Ran bool `json:"ran"` // Ran indicates if the execution ran
Conclusion core.Conclusion `json:"conclusion"` // Conclusion of the execution
Duration time.Duration `json:"duration"` // Duration of the execution
}

type WorkflowRunReport struct {
Ran bool `json:"ran"` // Ran indicates if the execution ran
Duration string `json:"duration"` // Duration of the execution
Name string `json:"name"` // Name is the name of the workflow
Path string `json:"path"` // Path is the path of the workflow
RunID string `json:"run_id"` // RunID is the ID of the run
RunNumber string `json:"run_number"` // RunNumber is the number of the run
RunAttempt string `json:"run_attempt"` // RunAttempt is the attempt number of the run
RetentionDays string `json:"retention_days"` // RetentionDays is the number of days to keep the run logs
Conclusion core.Conclusion `json:"conclusion"` // Conclusion is the result of a completed workflow run after continue-on-error is applied
Jobs map[string]core.Conclusion `json:"jobs"` // Jobs is map of the job run id to its result
}

// NewWorkflowRunReport creates a new workflow run report from the given workflow run.
func NewWorkflowRunReport(result *RunResult, wr *core.WorkflowRun) *WorkflowRunReport {
report := &WorkflowRunReport{
Ran: result.Ran,
Duration: result.Duration.String(),
Conclusion: result.Conclusion,
Name: wr.Workflow.Name,
Path: wr.Workflow.Path,
RunID: wr.RunID,
RunNumber: wr.RunNumber,
RunAttempt: wr.RunAttempt,
RetentionDays: wr.RetentionDays,
Jobs: make(map[string]core.Conclusion),
}

for id, job := range wr.Jobs {
report.Jobs[id] = job.Conclusion
}

return report
}

type JobRunReport struct {
Ran bool `json:"ran"` // Ran indicates if the execution ran
Duration string `json:"duration"` // Duration of the execution
Name string `json:"name"` // Name is the name of the job
RunID string `json:"run_id"` // RunID is the ID of the run
Conclusion core.Conclusion `json:"conclusion"` // Conclusion is the result of a completed job after continue-on-error is applied
Outcome core.Conclusion `json:"outcome"` // Outcome is the result of a completed job before continue-on-error is applied
Outputs map[string]string `json:"outputs,omitempty"` // Outputs is the outputs generated by the job
Matrix core.MatrixCombination `json:"matrix,omitempty"` // Matrix is the matrix parameters used to run the job
Steps []StepRunSummary `json:"steps"` // Steps is the list of steps in the job
}

type StepRunSummary struct {
ID string `json:"id"` // ID is the unique identifier of the step.
Name string `json:"name,omitempty"` // Name is the name of the step
Stage core.StepStage `json:"stage"` // Stage is the stage of the step during the execution of the job. Possible values are: setup, pre, main, post, complete.
Conclusion core.Conclusion `json:"conclusion"` // Conclusion is the result of a completed job after continue-on-error is applied
}

// NewJobRunReport creates a new job run report from the given job run.
func NewJobRunReport(result *RunResult, jr *core.JobRun) *JobRunReport {
report := &JobRunReport{
Ran: result.Ran,
Duration: result.Duration.String(),
Conclusion: result.Conclusion,
Name: jr.Job.Name,
RunID: jr.RunID,
Outcome: jr.Outcome,
Outputs: jr.Outputs,
Matrix: jr.Matrix,
}

for _, step := range jr.Steps {
summary := StepRunSummary{
ID: step.Step.ID,
Name: step.Step.Name,
Stage: step.Stage,
Conclusion: step.Conclusion,
}

report.Steps = append(report.Steps, summary)
}

return report
}

type StepRunReport struct {
Ran bool `json:"ran"` // Ran indicates if the execution ran
Duration string `json:"duration"` // Duration of the execution
ID string `json:"id"` // ID is the unique identifier of the step.
Name string `json:"name,omitempty"` // Name is the name of the step
Conclusion core.Conclusion `json:"conclusion"` // Conclusion is the result of a completed job after continue-on-error is applied
Outcome core.Conclusion `json:"outcome"` // Outcome is the result of a completed job before continue-on-error is applied
Outputs map[string]string `json:"outputs,omitempty"` // Outputs is the outputs generated by the job
State map[string]string `json:"state,omitempty"` // State is a map of step state variables.
Env map[string]string `json:"env,omitempty"` // Env is the extra environment variables set by the step.
Path []string `json:"path,omitempty"` // Path is extra PATH items set by the step.
}

// NewStepRunReport creates a new step run report from the given step run.
func NewStepRunReport(result *RunResult, sr *core.StepRun) *StepRunReport {
return &StepRunReport{
Ran: result.Ran,
Duration: result.Duration.String(),
ID: sr.Step.ID,
Name: sr.Step.Name,
Conclusion: result.Conclusion,
Outcome: sr.Outcome,
Outputs: sr.Outputs,
State: sr.State,
Env: sr.Environment,
Path: sr.Path,
}
}
16 changes: 9 additions & 7 deletions ghx/core/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ func (s *Step) Type() StepType {

// StepRun represents a single job run in a GitHub Actions workflow run
type StepRun struct {
Step Step `json:"step"` // Step is the step to run
Stage StepStage `json:"stage"` // Stage is the stage of the step during the execution of the job. Possible values are: setup, pre, main, post, complete.
Conclusion Conclusion `json:"conclusion"` // Conclusion is the result of a completed job after continue-on-error is applied
Outcome Conclusion `json:"outcome"` // Outcome is the result of a completed job before continue-on-error is applied
Outputs map[string]string `json:"outputs"` // Outputs is the outputs generated by the job
State map[string]string `json:"state"` // State is a map of step state variables.
Summary string `json:"summary"` // Summary is the summary of the step.
Step Step `json:"step"` // Step is the step to run
Stage StepStage `json:"stage"` // Stage is the stage of the step during the execution of the job. Possible values are: setup, pre, main, post, complete.
Conclusion Conclusion `json:"conclusion"` // Conclusion is the result of a completed job after continue-on-error is applied
Outcome Conclusion `json:"outcome"` // Outcome is the result of a completed job before continue-on-error is applied
Outputs map[string]string `json:"outputs"` // Outputs is the outputs generated by the job
State map[string]string `json:"state"` // State is a map of step state variables.
Summary string `json:"summary"` // Summary is the summary of the step.
Environment map[string]string `json:"environment"` // Environment is the extra environment variables set by the step.
Path []string `json:"path"` // Path is extra PATH items set by the step.
}
Loading

0 comments on commit 391478b

Please sign in to comment.