Skip to content

Commit

Permalink
Custom Formatters
Browse files Browse the repository at this point in the history
Taking the PR from @kim0 and expanding on it.

I wanted to be able to more easily test the formatters we have so far and make it easy to add more formatters later.

[#7]

Signed-off-by: Geoffrey Wiseman <[email protected]>
  • Loading branch information
geoffreywiseman committed Sep 21, 2023
1 parent 8ce4091 commit 3a7cfc0
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 71 deletions.
6 changes: 6 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ type Workflow struct {
State string
}

// WorkflowUsage is a map of usage by Workflow
type WorkflowUsage map[Workflow]uint

// RepoUsage is a map of WorkflowUsage by Repo
type RepoUsage map[*Repository]WorkflowUsage

// GetWorkflows returns a slice of Workflow instances, one for each workflow in the repository
func (c *Client) GetWorkflows(repository Repository) ([]Workflow, error) {
var page uint8 = 1
Expand Down
26 changes: 26 additions & 0 deletions format/formatters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package format

import (
"fmt"
"github.com/geoffreywiseman/gh-actions-usage/client"
"os"
)

var formatters = map[string]Formatter{
"human": humanFormatter{os.Stdout},
"tsv": tsvFormatter{os.Stdout},
}

// Formatter is an interface for formatting output from the extension, allowing the user to pick one of several output styles
type Formatter interface {
PrintUsage(usage client.RepoUsage)
}

// GetFormatter returns a formatter by name, or an error if the name is unknown
func GetFormatter(name string) (Formatter, error) {
f, ok := formatters[name]
if !ok {
return nil, fmt.Errorf("unknown formatter: %s", name)
}
return f, nil
}
28 changes: 28 additions & 0 deletions format/formatters_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package format

import (
"github.com/stretchr/testify/assert"
"testing"
)

func TestGetFormatters(t *testing.T) {
type test struct {
name string
expectedType interface{}
}
tests := []test{
{name: "human", expectedType: humanFormatter{}},
{name: "tsv", expectedType: tsvFormatter{}},
{name: "yaml"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
formatter, err := GetFormatter(tc.name)
if tc.expectedType == nil {
assert.Errorf(t, err, "unknown formatter %s", tc.name)
} else {
assert.IsType(t, tc.expectedType, formatter)
}
})
}
}
29 changes: 29 additions & 0 deletions format/human_formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package format

import (
"fmt"
"github.com/geoffreywiseman/gh-actions-usage/client"
"io"
)

type humanFormatter struct {
w io.Writer
}

func (hf humanFormatter) PrintUsage(usage client.RepoUsage) {
for repo, flowUsage := range usage {
var lines = make([]string, 0, len(flowUsage))
var repoTotal uint
for flow, usage := range flowUsage {
repoTotal += usage
line := fmt.Sprintf("- %s (%s, %s, %s)", flow.Name, flow.Path, flow.State, Humanize(usage))
lines = append(lines, line)
}
_, _ = fmt.Fprintf(hf.w, "%s (%d workflows; %s):\n", repo.FullName, len(usage[repo]), Humanize(repoTotal))
for _, line := range lines {
_, _ = fmt.Fprintln(hf.w, line)
}
_, _ = fmt.Fprintln(hf.w)

}
}
27 changes: 27 additions & 0 deletions format/human_formatter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package format

import (
"bytes"
"github.com/geoffreywiseman/gh-actions-usage/client"
"github.com/stretchr/testify/assert"
"testing"
)

func TestHumanFormatter(t *testing.T) {
// Given
var output bytes.Buffer
formatter := humanFormatter{&output}

wf := client.Workflow{Name: "CI", Path: ".github/workflows/ci.yml", State: "active"}
wfu := make(client.WorkflowUsage)
wfu[wf] = 50
r := client.Repository{FullName: "codiform/gh-actions-usage"}
ru := make(client.RepoUsage)
ru[&r] = wfu

// When
formatter.PrintUsage(ru)

// Then
assert.Equal(t, "codiform/gh-actions-usage (1 workflows; 50ms):\n- CI (.github/workflows/ci.yml, active, 50ms)\n\n", output.String())
}
20 changes: 20 additions & 0 deletions format/tsv_formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package format

import (
"fmt"
"github.com/geoffreywiseman/gh-actions-usage/client"
"io"
)

type tsvFormatter struct {
w io.Writer
}

func (tf tsvFormatter) PrintUsage(usage client.RepoUsage) {
_, _ = fmt.Fprintf(tf.w, "%s\t%s\t%s\n", "Repo", "Workflow", "Milliseconds")
for repo, flowUsage := range usage {
for workflow, usage := range flowUsage {
_, _ = fmt.Fprintf(tf.w, "%s\t%s\t%d\n", repo.FullName, workflow.Path, usage)
}
}
}
27 changes: 27 additions & 0 deletions format/tsv_formatter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package format

import (
"bytes"
"github.com/geoffreywiseman/gh-actions-usage/client"
"github.com/stretchr/testify/assert"
"testing"
)

func TestTsvFormatter(t *testing.T) {
// Given
var output bytes.Buffer
formatter := tsvFormatter{&output}

wf := client.Workflow{Name: "Security", Path: ".github/workflows/DevSecOps.yaml", State: "alert"}
wfu := make(client.WorkflowUsage)
wfu[wf] = 2500
r := client.Repository{FullName: "codiform/gh-actions-usage"}
ru := make(client.RepoUsage)
ru[&r] = wfu

// When
formatter.PrintUsage(ru)

// Then
assert.Equal(t, "Repo\tWorkflow\tMilliseconds\ncodiform/gh-actions-usage\t.github/workflows/DevSecOps.yaml\t2500\n", output.String())
}
99 changes: 28 additions & 71 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,41 @@ package main
import (
"flag"
"fmt"
"github.com/geoffreywiseman/gh-actions-usage/client"
"github.com/geoffreywiseman/gh-actions-usage/format"
"log"
"runtime/debug"
"strings"
"time"

"github.com/geoffreywiseman/gh-actions-usage/client"
"github.com/geoffreywiseman/gh-actions-usage/format"
)

var gh client.Client

type Config struct {
Output string
Skip bool
type config struct {
output string
skip bool
format format.Formatter
}

func main() {
fmt.Printf("GitHub Actions Usage (%s)\n\n", getVersion())

gh = client.New()

config := &Config{}
flag.BoolVar(&config.Skip, "skip", false, "Skips displaying repositories with no workflows")
flag.StringVar(&config.Output, "output", "human", "output format: human or TSV (machine readable)")
cfg := &config{}
flag.BoolVar(&cfg.skip, "skip", false, "Skips displaying repositories with no workflows")
flag.StringVar(&cfg.output, "output", "human", "Output format: human or TSV (machine readable)")
flag.Parse()
if config.Output != "human" && config.Output != "tsv" {
log.Fatal("Invalid output format. Choose 'human' or 'tsv'.")

var err error
cfg.format, err = format.GetFormatter(cfg.output)
if err != nil {
log.Fatal(err)
}

if len(flag.Args()) < 1 {
tryDisplayCurrentRepo(*config)
tryDisplayCurrentRepo(*cfg)
} else {
tryDisplayAllSpecified(*config, flag.Args())
tryDisplayAllSpecified(*cfg, flag.Args())
}
}

Expand All @@ -56,76 +58,34 @@ func getVersion() string {
return "?"
}

func tryDisplayCurrentRepo(config Config) {
func tryDisplayCurrentRepo(cfg config) {
repo, err := gh.GetCurrentRepository()
if repo == nil {
fmt.Printf("No current repository: %s\n\n", err)
printUsage()
printHelp()
return
}
var repoFlowUsage = make(map[*client.Repository]workflowUsage)
var repoFlowUsage = make(map[*client.Repository]client.WorkflowUsage)
r := getRepoUsage(false, repo)
repoFlowUsage[repo] = r
printRepoFlowUsage(config, repoFlowUsage)
cfg.format.PrintUsage(repoFlowUsage)
}

func tryDisplayAllSpecified(config Config, targets []string) {
func tryDisplayAllSpecified(cfg config, targets []string) {
repos, err := getRepositories(targets)
if err != nil {
fmt.Printf("Error getting targets: %s\n\n", err)
printUsage()
printHelp()
return
}
var repoFlowUsage = make(map[*client.Repository]workflowUsage)
var repoFlowUsage = make(map[*client.Repository]client.WorkflowUsage)
for _, list := range repos {
for _, item := range list {
r := getRepoUsage(config.Skip, item)
r := getRepoUsage(cfg.skip, item)
repoFlowUsage[item] = r
}
}
printRepoFlowUsage(config, repoFlowUsage)
}

func printRepoFlowUsage(config Config, results repoFlowUsage) {
switch config.Output {
case "tsv":
printRepoFlowUsage_tsv(results)
default:
printRepoFlowUsage_human(results)
}

}

func printRepoFlowUsage_tsv(results repoFlowUsage) {
fmt.Printf("%s\t%s\t%s\n", "Repo", "Workflow", "Minutes")
for repo, flowUsage := range results {
for workflow, usage := range flowUsage {
d := time.Duration(usage) * time.Millisecond
fmt.Printf("%s\t%s\t%d\n", repo.FullName, workflow.Path, int(d.Minutes()))
}
}
fmt.Println("")
}

func printRepoFlowUsage_human(results repoFlowUsage) {

for repo, flowUsage := range results {
var lines = make([]string, 0, len(flowUsage))
var repoTotal uint
for flow, usage := range flowUsage {
repoTotal += usage
line := fmt.Sprintf("- %s (%s, %s, %s)", flow.Name, flow.Path, flow.State, format.Humanize(usage))
lines = append(lines, line)
// result[flow] = usage.TotalMs()
}
fmt.Printf("%s (%d workflows; %s): \n", repo.FullName, len(results[repo]), format.Humanize(repoTotal))
for _, line := range lines {
fmt.Println(line)
}
fmt.Println()

}

cfg.format.PrintUsage(repoFlowUsage)
}

type repoMap map[*client.User][]*client.Repository
Expand Down Expand Up @@ -190,16 +150,13 @@ func mapOwner(repos repoMap, userName string) error {
return nil
}

type workflowUsage map[client.Workflow]uint
type repoFlowUsage map[*client.Repository]workflowUsage

func getRepoUsage(skip bool, repo *client.Repository) workflowUsage {
func getRepoUsage(skip bool, repo *client.Repository) client.WorkflowUsage {
workflows, err := gh.GetWorkflows(*repo)
if err != nil {
panic(err)
}

var result = make(workflowUsage)
var result = make(client.WorkflowUsage)
for _, flow := range workflows {
usage, err := gh.GetWorkflowUsage(*repo, flow)
if err != nil {
Expand All @@ -212,7 +169,7 @@ func getRepoUsage(skip bool, repo *client.Repository) workflowUsage {
return result
}

func printUsage() {
func printHelp() {
fmt.Println("USAGE: gh actions-usage [--output=human|tsv] [--skip] [target]...\n\n" +
"Gets the usage for all workflows in one or more GitHub repositories.\n\n" +
"If target is not specified, actions-usage will attempt to get usage for a git repo in the current working directory.\n" +
Expand Down

0 comments on commit 3a7cfc0

Please sign in to comment.