From 13ca0637be05ce9ad32390907fa87aadb195e283 Mon Sep 17 00:00:00 2001 From: Dhruv Thakur Date: Sat, 10 Aug 2024 14:36:34 +0200 Subject: [PATCH 01/10] feat: use current directory by default --- .github/workflows/build.yml | 6 +- .github/workflows/release.yml | 11 +- .gitignore | 1 + .golangci.yml | 21 ++++ .goreleaser.yaml | 14 ++- README.md | 6 +- cmd/current.go | 101 +++++++++++++++ cmd/current_test.go | 92 ++++++++++++++ cmd/gh.go | 29 ----- cmd/help.go | 15 +-- cmd/root.go | 131 ++++++++++++++----- examples/html/act3.yml | 53 -------- go.mod | 32 ++++- go.sum | 153 ++++++++++++++++++++++- internal/gh/gh.go | 25 ++++ main.go | 9 +- ui/assets/error.html | 11 ++ ui/{template.go => assets/template.html} | 24 +--- ui/cmds.go | 13 +- ui/config.go | 11 ++ ui/initial.go | 16 +-- ui/model.go | 16 +-- ui/styles.go | 54 ++++---- ui/types.go | 25 ++-- ui/ui.go | 8 +- ui/update.go | 11 +- ui/view.go | 78 +++++++----- 27 files changed, 695 insertions(+), 271 deletions(-) create mode 100644 .golangci.yml create mode 100644 cmd/current.go create mode 100644 cmd/current_test.go delete mode 100644 cmd/gh.go create mode 100644 internal/gh/gh.go create mode 100644 ui/assets/error.html rename ui/{template.go => assets/template.html} (93%) create mode 100644 ui/config.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4c076ff..a902b23 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,9 +25,11 @@ jobs: uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} - - name: go build - run: go build -v ./... - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: version: v1.58 + - name: go build + run: go build -v ./... + - name: go test + run: go test -v ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5b92dc0..4dbaf07 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,9 @@ on: tags: - 'v*' +permissions: + id-token: write + env: GO_VERSION: '1.22.5' @@ -21,15 +24,12 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Build run: go build -v ./... + - name: Test + run: go test -v ./... - name: Install Cosign uses: sigstore/cosign-installer@v3 with: cosign-release: 'v2.2.3' - - name: Store Cosign private key in a file - run: 'echo "$COSIGN_KEY" > cosign.key' - shell: bash - env: - COSIGN_KEY: ${{secrets.COSIGN_KEY}} - name: Release Binaries uses: goreleaser/goreleaser-action@v6 with: @@ -37,4 +37,3 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{secrets.GH_PAT}} - COSIGN_PASSWORD: ${{secrets.COSIGN_PASSWORD}} diff --git a/.gitignore b/.gitignore index a39004d..de1d1cb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ cosign.key cosign.pub justfile examples/html/docs/sample.html +.cmds diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..c52fd12 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,21 @@ +linters: + enable: + - errcheck + - errname + - errorlint + - goconst + - gofumpt + - gosimple + - govet + - ineffassign + - nilerr + - prealloc + - predeclared + - revive + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - unused + - usestdlibvars + - wastedassign diff --git a/.goreleaser.yaml b/.goreleaser.yaml index b1aa4da..5649d6b 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,5 +1,8 @@ version: 2 +release: + draft: true + before: hooks: - go mod tidy @@ -14,15 +17,16 @@ builds: signs: - cmd: cosign - stdin: "{{.Env.COSIGN_PASSWORD}}" + signature: "${artifact}.sig" + certificate: "${artifact}.pem" args: - "sign-blob" - - "--key=cosign.key" + - "--oidc-issuer=https://token.actions.githubusercontent.com" + - "--output-certificate=${certificate}" - "--output-signature=${signature}" - "${artifact}" - - "--yes" # needed on cosign 2.0.0+ - artifacts: all - + - "--yes" + artifacts: checksum brews: - name: act3 diff --git a/README.md b/README.md index ea8f4b9..b22a6a7 100644 --- a/README.md +++ b/README.md @@ -71,18 +71,18 @@ workflows: `{{runNumber}}` gets replaced with the actual run number of the workflow. -You can find the ID for your workflow as follows: +You can find the ID for a workflow as follows: ```bash curl -L \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer " \ -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos///actions/workflows/ + https://api.github.com/repos///actions/workflows # or -gh api repos///actions/workflows/ +gh api repos///actions/workflows # use node_id from the response ``` diff --git a/cmd/current.go b/cmd/current.go new file mode 100644 index 0000000..2a1b6ea --- /dev/null +++ b/cmd/current.go @@ -0,0 +1,101 @@ +package cmd + +// +import ( + "errors" + "fmt" + "net/url" + "strings" + + ghapi "github.com/cli/go-gh/v2/pkg/api" + "github.com/dhth/act3/internal/gh" + "github.com/dhth/act3/ui" + "github.com/go-git/go-git/v5" +) + +var ( + errCouldntGetRepo = errors.New("couldn't get repository") + errNoRemotesFound = errors.New("no remotes found") + errRemoteURLEmpty = errors.New("remote URL is empty") + errInvalidURLFormat = errors.New("remote URL has invalid format") + errCouldntParseRemoteURL = errors.New("couldn't parse remote URL") +) + +func getWorkflowsForCurrentRepo(ghClient *ghapi.RESTClient, repo string) ([]ui.Workflow, error) { + wd, err := gh.GetWorkflowDetails(ghClient, repo) + if err != nil { + return nil, err + } + + workflows := make([]ui.Workflow, len(wd.Workflows)) + for i, w := range wd.Workflows { + workflows[i] = ui.Workflow{ + ID: w.NodeID, + Repo: repo, + Name: w.Name, + } + } + + return workflows, nil +} + +func getCurrentRepo() (string, error) { + repo, err := git.PlainOpen(".") + if err != nil { + return "", fmt.Errorf("%w: %s", errCouldntGetRepo, err.Error()) + } + + remotes, err := repo.Remotes() + if err != nil { + return "", fmt.Errorf("%w: %s", errNoRemotesFound, err.Error()) + } + + if len(remotes) == 0 { + return "", fmt.Errorf("%w", errNoRemotesFound) + } + + remote := remotes[0] + if remote == nil { + return "", fmt.Errorf("%w", errNoRemotesFound) + } + + remoteURL := remote.Config().URLs[0] + + userRepo, err := extractRepoName(remoteURL) + if err != nil { + return "", fmt.Errorf("%w: %s", errCouldntParseRemoteURL, err.Error()) + } + + return userRepo, nil +} + +func extractRepoName(remoteURL string) (string, error) { + if remoteURL == "" { + return "", fmt.Errorf("%w", errRemoteURLEmpty) + } + if strings.HasPrefix(remoteURL, "git@") { + parts := strings.Split(remoteURL, ":") + if len(parts) < 2 { + return "", fmt.Errorf("%w", errInvalidURLFormat) + } + return strings.TrimSuffix(parts[1], ".git"), nil + } + + parsedURL, err := url.Parse(remoteURL) + if err != nil { + return "", fmt.Errorf("%w: %s", errInvalidURLFormat, err.Error()) + } + + if parsedURL.Scheme == "" { + return "", fmt.Errorf("%w: URL scheme is empty", errInvalidURLFormat) + } + + if strings.Count(parsedURL.Path, "/") > 2 { + return "", fmt.Errorf("%w", errInvalidURLFormat) + } + + userRepo := strings.TrimSuffix(parsedURL.Path, ".git") + userRepo = strings.TrimPrefix(userRepo, "/") + + return userRepo, nil +} diff --git a/cmd/current_test.go b/cmd/current_test.go new file mode 100644 index 0000000..f5470ce --- /dev/null +++ b/cmd/current_test.go @@ -0,0 +1,92 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExtractRepoName(t *testing.T) { + expectedRepo := "dhth/act3" + testCases := []struct { + name string + input string + expected string + err error + }{ + // success + { + name: "https url", + input: "https://github.com/dhth/act3.git", + expected: expectedRepo, + }, + { + name: "https url without .git suffix", + input: "https://github.com/dhth/act3", + expected: expectedRepo, + }, + { + name: "http url", + input: "http://github.com/dhth/act3.git", + expected: expectedRepo, + }, + { + name: "ssh url", + input: "ssh://git@github.com/dhth/act3.git", + expected: expectedRepo, + }, + { + name: "ssh url with port", + input: "ssh://git@github.com:443/dhth/act3.git", + expected: expectedRepo, + }, + { + name: "git protocol url", + input: "git://github.com/dhth/act3.git", + expected: expectedRepo, + }, + { + name: "git protocol url with port", + input: "git://github.com:9418/dhth/act3.git", + expected: expectedRepo, + }, + // failures + { + name: "empty url", + input: "", + err: errRemoteURLEmpty, + }, + { + name: "invalid url", + input: "invalid_url", + err: errInvalidURLFormat, + }, + { + name: "url with no scheme", + input: "github.com/dhth/act3.git", + err: errInvalidURLFormat, + }, + { + name: "ssh url with subdirectory", + input: "https://github.com/dhth/act3/subdir", + err: errInvalidURLFormat, + }, + { + name: "https url with subdirectory", + input: "https://github.com/dhth/act3/subdir", + err: errInvalidURLFormat, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got, err := extractRepoName(tt.input) + if tt.err == nil { + assert.NoError(t, err) + assert.Equal(t, tt.expected, got) + } else { + assert.ErrorIs(t, err, tt.err) + } + }) + } +} diff --git a/cmd/gh.go b/cmd/gh.go deleted file mode 100644 index 14f6554..0000000 --- a/cmd/gh.go +++ /dev/null @@ -1,29 +0,0 @@ -package cmd - -import ( - "context" - "errors" - "os" - - "github.com/shurcooL/githubv4" - "golang.org/x/oauth2" -) - -var ( - GHTokenNotProvided = errors.New("Github Access Token not provided") -) - -func getGHClient() (*githubv4.Client, error) { - accessToken := os.Getenv("ACT3_GH_ACCESS_TOKEN") - - if accessToken == "" { - return nil, GHTokenNotProvided - } - src := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: accessToken}, - ) - httpClient := oauth2.NewClient(context.Background(), src) - - client := githubv4.NewClient(httpClient) - return client, nil -} diff --git a/cmd/help.go b/cmd/help.go index b53cb37..48661b3 100644 --- a/cmd/help.go +++ b/cmd/help.go @@ -1,7 +1,5 @@ package cmd -import "fmt" - var ( configSampleFormat = ` workflows: @@ -18,18 +16,7 @@ workflows: name: release key: cueitup:release ` - helpText = `Glance at the last 3 runs of your Github Actions + helpText = `Glance at the last 3 runs of your Github Actions. Usage: act3 [flags]` ) - -func cfgErrSuggestion(msg string) string { - return fmt.Sprintf(`%s - -Make sure to structure the config file as follows: -%s -Use "act3 -help" for more information`, - msg, - configSampleFormat, - ) -} diff --git a/cmd/root.go b/cmd/root.go index 731f40c..5270ee2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,31 +1,65 @@ package cmd import ( + "errors" + "flag" "fmt" "os" - "os/user" - - "flag" + "path/filepath" + "runtime" + "time" + ghapi "github.com/cli/go-gh/v2/pkg/api" "github.com/dhth/act3/ui" ) -func die(msg string, args ...any) { - fmt.Fprintf(os.Stderr, msg+"\n", args...) - os.Exit(1) -} +const ( + configPath = "act3/act3.yml" + author = "@dhth" + projectHomePage = "https://github.com/dhth/act3" + issuesURL = "https://github.com/dhth/act3/issues" +) + +var ( + errCouldntGetConfigDir = errors.New("couldn't get your config directory") + errConfigFilePathEmpty = errors.New("config file path is empty") + errIncorrectOutputFmt = errors.New("incorrect value for output format provided") + errConfigFileDoesntExit = errors.New("config file doesn't exist") + errCouldntReadConfig = errors.New("couldn't read config") + errCouldntGetGHClient = errors.New("couldn't get a Github client") + errNoWorkflows = errors.New("no workflows found") + errTemplateFileDoesntExit = errors.New("template file doesn't exist") + errCouldntReadTemplateFile = errors.New("couldn't read template file") + errCouldntGetWorkflows = errors.New("couldn't get workflows") +) var ( format = flag.String("format", "", "output format to use; possible values: html") htmlTemplateFile = flag.String("html-template-file", "", "path of the HTML template file to use") + global = flag.Bool("g", false, "whether to use workflows defined globally via the config file") ) -func Execute() { - currentUser, err := user.Current() - var defaultConfigFilePath string - if err == nil { - defaultConfigFilePath = fmt.Sprintf("%s/.config/act3/act3.yml", currentUser.HomeDir) +func Execute() error { + var defaultConfigDir string + var configErr error + switch runtime.GOOS { + case "linux", "windows": + defaultConfigDir, configErr = os.UserConfigDir() + default: + hd, configErr := os.UserHomeDir() + if configErr != nil { + break + } + defaultConfigDir = filepath.Join(hd, ".config") + } + if configErr != nil { + fmt.Printf(`Couldn't get your default config directory. This is a fatal error; +use --config-file to specify config file path manually. +Let %s know about this via %s. +`, author, issuesURL) + return fmt.Errorf("%w: %s", errCouldntGetConfigDir, configErr.Error()) } + defaultConfigFilePath := filepath.Join(defaultConfigDir, configPath) configFilePath := flag.String("config-file", defaultConfigFilePath, "path of the config file") flag.Usage = func() { @@ -36,7 +70,7 @@ func Execute() { flag.Parse() if *configFilePath == "" { - die("config-file cannot be empty") + return fmt.Errorf("%w", errConfigFilePathEmpty) } var outputFmt ui.OutputFmt @@ -45,42 +79,83 @@ func Execute() { case "html": outputFmt = ui.HTMLFmt default: - die("unsupported value for format") + return fmt.Errorf("%w", errIncorrectOutputFmt) } } - configFilePathExpanded := expandTilde(*configFilePath) - - _, err = os.Stat(configFilePathExpanded) - if os.IsNotExist(err) { - die(cfgErrSuggestion(fmt.Sprintf("Error: file doesn't exist at %q", configFilePathExpanded))) + clientOpts := ghapi.ClientOptions{ + EnableCache: true, + CacheTTL: time.Second * 30, + Timeout: 8 * time.Second, } + var workflows []ui.Workflow + var currentRepo string + var err error - workflows, err := ReadConfig(configFilePathExpanded) - if err != nil { - die(cfgErrSuggestion(fmt.Sprintf("Error reading config: %v", configFilePathExpanded))) + if *global { + configFilePathExpanded := expandTilde(*configFilePath) + + _, err = os.Stat(configFilePathExpanded) + if os.IsNotExist(err) { + return fmt.Errorf("%w: path: %s", errConfigFileDoesntExit, configFilePathExpanded) + } + + workflows, err = ReadConfig(configFilePathExpanded) + if err != nil { + fmt.Print(configSampleFormat) + return fmt.Errorf("%w: %s", errCouldntReadConfig, err.Error()) + } + + } else { + currentRepo, err = getCurrentRepo() + if err != nil { + return err + } + ghRClient, err := ghapi.NewRESTClient(clientOpts) + if err != nil { + return fmt.Errorf("%w: %s", errCouldntGetGHClient, err.Error()) + } + + workflows, err = getWorkflowsForCurrentRepo(ghRClient, currentRepo) + if err != nil { + return fmt.Errorf("%w: %s", errCouldntGetWorkflows, err.Error()) + } } + if len(workflows) == 0 { - die(cfgErrSuggestion("No workflows found")) + return fmt.Errorf("%w", errNoWorkflows) } var htmlTemplate string if *htmlTemplateFile != "" { _, err := os.Stat(*htmlTemplateFile) if os.IsNotExist(err) { - die(fmt.Sprintf("Error: template file doesn't exist at %q", *htmlTemplateFile)) + return fmt.Errorf("%w: path: %s", errTemplateFileDoesntExit, *htmlTemplateFile) } templateFileContents, err := os.ReadFile(*htmlTemplateFile) if err != nil { - die(fmt.Sprintf("Error: couldn't read template file %q", *htmlTemplateFile)) + return fmt.Errorf("%w: %s", errCouldntReadTemplateFile, err.Error()) } htmlTemplate = string(templateFileContents) } - ghClient, err := getGHClient() + ghClient, err := ghapi.NewGraphQLClient(clientOpts) if err != nil { - die("Error: %q", err.Error()) + return fmt.Errorf("%w: %s", errCouldntGetGHClient, err.Error()) + } + + var cr *string + if !*global { + cr = ¤tRepo + } + config := ui.Config{ + GHClient: ghClient, + Workflows: workflows, + CurrentRepo: cr, + Fmt: outputFmt, + HTMLTemplate: htmlTemplate, } - ui.RenderUI(ghClient, workflows, outputFmt, htmlTemplate) + ui.RenderUI(config) + return nil } diff --git a/examples/html/act3.yml b/examples/html/act3.yml index ca6e52b..7e9ac6a 100644 --- a/examples/html/act3.yml +++ b/examples/html/act3.yml @@ -5,27 +5,14 @@ workflows: name: build url: https://asampleurl.com/{{runNumber}} -- id: W_kwDOLkC0eM4FaKWA - repo: dhth/act3 - name: release - url: https://asampleurl.com/{{runNumber}} - - id: W_kwDOLvB80c4Fmr3Y repo: dhth/commits name: build -- id: W_kwDOLvB80c4Fmr3Z - repo: dhth/commits - name: release - - id: W_kwDOLb3Pms4FRxjX repo: dhth/cueitup name: build -- id: W_kwDOLb3Pms4FRxjY - repo: dhth/cueitup - name: release - - id: W_kwDOLqVZts4FhIc3 repo: dhth/dstll name: build @@ -34,70 +21,30 @@ workflows: repo: dhth/ecsv name: build -- id: W_kwDOLghtl84GMbuw - repo: dhth/ecsv - name: vulncheck - -- id: W_kwDOLghtl84FWTla - repo: dhth/ecsv - name: release - - id: W_kwDOMG_5As4GEdgo repo: dhth/hours name: build -- id: W_kwDOMG_5As4GEdgp - repo: dhth/hours - name: release - - id: W_kwDOLa0twM4FRynm repo: dhth/kplay name: build -- id: W_kwDOLa0twM4FRynn - repo: dhth/kplay - name: release - - id: W_kwDOL0-ubM4FuRN3 repo: dhth/mult name: build -- id: W_kwDOL0-ubM4FuRN4 - repo: dhth/mult - name: release - - id: W_kwDOLafHJ84FQglT repo: dhth/outtasync name: build -- id: W_kwDOLafHJ84FQglU - repo: dhth/outtasync - name: release - - id: W_kwDOLnOhHM4FdsSJ repo: dhth/prs name: build -- id: W_kwDOLnOhHM4FdsSK - repo: dhth/prs - name: release - - id: W_kwDOLe-lJM4FUlXo repo: dhth/punchout name: build -- id: W_kwDOLe-lJM4FVCrl - repo: dhth/punchout - name: release - - id: W_kwDOLjRZQ84FZMD2 repo: dhth/schemas name: build - -- id: W_kwDOLjRZQ84GMcOk - repo: dhth/schemas - name: vulncheck - -- id: W_kwDOLjRZQ84FZMD3 - repo: dhth/schemas - name: release diff --git a/go.mod b/go.mod index c609048..2337349 100644 --- a/go.mod +++ b/go.mod @@ -5,19 +5,36 @@ go 1.22.5 require ( github.com/charmbracelet/bubbletea v0.26.6 github.com/charmbracelet/lipgloss v0.12.1 + github.com/cli/go-gh/v2 v2.9.0 + github.com/cli/shurcooL-graphql v0.0.4 github.com/dustin/go-humanize v1.0.1 + github.com/go-git/go-git/v5 v5.12.0 github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 - golang.org/x/oauth2 v0.22.0 + github.com/stretchr/testify v1.9.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/x/ansi v0.1.4 // indirect github.com/charmbracelet/x/input v0.1.3 // indirect github.com/charmbracelet/x/term v0.1.1 // indirect github.com/charmbracelet/x/windows v0.1.2 // indirect + github.com/cli/safeexec v1.0.1 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/henvic/httpretty v0.1.3 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -25,10 +42,23 @@ require ( github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.15.2 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect + github.com/thlib/go-timezone-local v0.0.3 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/oauth2 v0.22.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index d62fac6..724c8f1 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,17 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s= github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk= github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs= @@ -12,12 +24,57 @@ github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXD github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= +github.com/cli/go-gh/v2 v2.9.0 h1:D3lTjEneMYl54M+WjZ+kRPrR5CEJ5BHS05isBPOV3LI= +github.com/cli/go-gh/v2 v2.9.0/go.mod h1:MeRoKzXff3ygHu7zP+NVTT+imcHW6p3tpuxHAzRM2xE= +github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= +github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= +github.com/cli/shurcooL-graphql v0.0.4 h1:6MogPnQJLjKkaXPyGqPRXOI2qCsQdqNfUY1QSJu2GuY= +github.com/cli/shurcooL-graphql v0.0.4/go.mod h1:3waN4u02FiZivIV+p1y4d0Jo1jc6BViMA73C+sZo2fk= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/henvic/httpretty v0.1.3 h1:4A6vigjz6Q/+yAfTD4wqipCv+Px69C7Th/NhT0ApuU8= +github.com/henvic/httpretty v0.1.3/go.mod h1:UUEv7c2kHZ5SPQ51uS3wBpzPDibg2U3Y+IaXyHy5GBg= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -32,28 +89,118 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkvclm+pWm1Lk4YrREb4IOIb/YdFO0p2M= github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8= github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0= github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/thlib/go-timezone-local v0.0.3 h1:ie5XtZWG5lQ4+1MtC5KZ/FeWlOKzW2nPoUnXYUbV/1s= +github.com/thlib/go-timezone-local v0.0.3/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/gh/gh.go b/internal/gh/gh.go new file mode 100644 index 0000000..9ea4585 --- /dev/null +++ b/internal/gh/gh.go @@ -0,0 +1,25 @@ +package gh + +import ( + "fmt" + + ghapi "github.com/cli/go-gh/v2/pkg/api" +) + +type WorkflowDetailsResult struct { + NodeID string `json:"node_id"` + Name string + State string +} + +type WorkflowDetails struct { + TotalCount int + Workflows []WorkflowDetailsResult +} + +func GetWorkflowDetails(ghClient *ghapi.RESTClient, repo string) (WorkflowDetails, error) { + // https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#list-repository-workflows + var wd WorkflowDetails + err := ghClient.Get(fmt.Sprintf("repos/%s/actions/workflows", repo), &wd) + return wd, err +} diff --git a/main.go b/main.go index 0f15f30..41d9b1e 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,16 @@ package main import ( + "fmt" + "os" + "github.com/dhth/act3/cmd" ) func main() { - cmd.Execute() + err := cmd.Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) + os.Exit(1) + } } diff --git a/ui/assets/error.html b/ui/assets/error.html new file mode 100644 index 0000000..317a39a --- /dev/null +++ b/ui/assets/error.html @@ -0,0 +1,11 @@ + + + + + + + +

Something went wrong generating the HTML

+

Error: %s

+ + diff --git a/ui/template.go b/ui/assets/template.html similarity index 93% rename from ui/template.go rename to ui/assets/template.html index 6b0965a..d3e0f93 100644 --- a/ui/template.go +++ b/ui/assets/template.html @@ -1,7 +1,3 @@ -package ui - -const ( - HTMLTemplText = ` @@ -13,6 +9,9 @@

{{.Title}}

+ {{if .CurrentRepo}} +

{{.CurrentRepo}}

+ {{end}}

Generated at {{.Timestamp}}

@@ -25,7 +24,7 @@

{{.Title}}

{{range .Rows -}} - + {{range .Data -}} {{if .Error}} {{else if .Success}} {{else}}
{{.Key}}{{.Key}} @@ -102,18 +101,3 @@

{{.Title}}

-` - errorTemplate = ` - - - - - - - -

Something went wrong generating the HTML

-

Error: %s

- - -` -) diff --git a/ui/cmds.go b/ui/cmds.go index bb6b84e..6e3422b 100644 --- a/ui/cmds.go +++ b/ui/cmds.go @@ -1,20 +1,19 @@ package ui import ( - "context" - tea "github.com/charmbracelet/bubbletea" - "github.com/shurcooL/githubv4" + ghapi "github.com/cli/go-gh/v2/pkg/api" + ghgql "github.com/cli/shurcooL-graphql" ) -func getWorkflowRuns(ghClient *githubv4.Client, workflow Workflow) tea.Cmd { +func getWorkflowRuns(ghClient *ghapi.GraphQLClient, workflow Workflow) tea.Cmd { return func() tea.Msg { variables := map[string]interface{}{ - "numWorkflowRuns": githubv4.Int(3), - "workflowId": githubv4.ID(workflow.ID), + "numWorkflowRuns": ghgql.Int(3), + "workflowId": ghgql.ID(workflow.ID), } var query QueryResult - err := ghClient.Query(context.Background(), &query, variables) + err := ghClient.Query("GetWorkflows", &query, variables) return WorkflowRunsFetchedMsg{workflow, query, err} } diff --git a/ui/config.go b/ui/config.go new file mode 100644 index 0000000..026e806 --- /dev/null +++ b/ui/config.go @@ -0,0 +1,11 @@ +package ui + +import ghapi "github.com/cli/go-gh/v2/pkg/api" + +type Config struct { + GHClient *ghapi.GraphQLClient + Workflows []Workflow + CurrentRepo *string + Fmt OutputFmt + HTMLTemplate string +} diff --git a/ui/initial.go b/ui/initial.go index 67be2bd..99444ea 100644 --- a/ui/initial.go +++ b/ui/initial.go @@ -1,24 +1,14 @@ package ui -import "github.com/shurcooL/githubv4" - -const ( - NUM_RUNS_TO_DISPLAY = 3 -) - -func InitialModel(ghClient *githubv4.Client, workflows []Workflow, outputFmt OutputFmt, htmlTemplate string) model { - +func InitialModel(config Config) Model { workflowResults := make(map[string]workflowRunResults) errors := make([]error, 0) failedWorkflowRunURLs := make(map[string]string) - m := model{ - ghClient: ghClient, - workflows: workflows, + m := Model{ + config: config, workFlowResults: workflowResults, - outputFmt: outputFmt, - htmlTemplate: htmlTemplate, message: "hello", errors: errors, failedWorkflowURLs: failedWorkflowRunURLs, diff --git a/ui/model.go b/ui/model.go index a50e790..55016bb 100644 --- a/ui/model.go +++ b/ui/model.go @@ -2,26 +2,22 @@ package ui import ( tea "github.com/charmbracelet/bubbletea" - "github.com/shurcooL/githubv4" ) -type model struct { - workflows []Workflow - ghClient *githubv4.Client +type Model struct { + config Config workFlowResults map[string]workflowRunResults numResults int - outputFmt OutputFmt message string - htmlTemplate string errors []error failedWorkflowURLs map[string]string outputPrinted bool } -func (m model) Init() tea.Cmd { - var cmds []tea.Cmd - for _, workflow := range m.workflows { - cmds = append(cmds, getWorkflowRuns(m.ghClient, workflow)) +func (m Model) Init() tea.Cmd { + cmds := make([]tea.Cmd, len(m.config.Workflows)) + for i, workflow := range m.config.Workflows { + cmds[i] = getWorkflowRuns(m.config.GHClient, workflow) } return tea.Batch(cmds...) } diff --git a/ui/styles.go b/ui/styles.go index 8eaa8ad..37906e1 100644 --- a/ui/styles.go +++ b/ui/styles.go @@ -3,33 +3,39 @@ package ui import "github.com/charmbracelet/lipgloss" const ( - BACKGROUND_COLOR = "#282828" - HEADER_COLOR = "#fe8019" - RUN_NUMBER_COLOR = "#83a598" - WORKFLOW_COLOR = "#d3869b" - SUCCESS_COLOR = "#b8bb26" - FAILURE_COLOR = "#fb4934" - ERROR_COLOR = "#fabd2f" - ERROR_DETAIL_COLOR = "#665c54" - CONTEXT_COLOR = "#665c54" + bgColor = "#282828" + headerColor = "#fe8019" + runNumberColor = "#83a598" + workflowColor = "#d3869b" + successColor = "#b8bb26" + failureColor = "#fb4934" + errorColor = "#fabd2f" + errorDetailColor = "#665c54" + contextColor = "#665c54" + currentRepoColor = "#b8bb26" ) var ( fgStyle = lipgloss.NewStyle(). PaddingLeft(1). PaddingRight(1). - Foreground(lipgloss.Color(BACKGROUND_COLOR)) + Foreground(lipgloss.Color(bgColor)) headerStyle = fgStyle. Align(lipgloss.Center). Bold(true). - Background(lipgloss.Color(HEADER_COLOR)) + Background(lipgloss.Color(headerColor)) + + currentRepoStyle = fgStyle. + PaddingLeft(1). + Bold(true). + Foreground(lipgloss.Color(currentRepoColor)) runNumberStyle = fgStyle. Align(lipgloss.Center). Bold(true). - Background(lipgloss.Color(RUN_NUMBER_COLOR)). - Width(RUN_NUMBER_WIDTH) + Background(lipgloss.Color(runNumberColor)). + Width(runNumberWidth) nonFgStyle = lipgloss.NewStyle(). PaddingLeft(1). @@ -38,36 +44,36 @@ var ( workflowStyle = nonFgStyle. Align(lipgloss.Left). Bold(true). - Foreground(lipgloss.Color(WORKFLOW_COLOR)). - Width(WORKFLOW_NAME_WIDTH) + Foreground(lipgloss.Color(workflowColor)). + Width(workflowNameWidth + 4) runResultStyle = nonFgStyle. - PaddingLeft((RUN_NUMBER_WIDTH - 20) / 2). // TODO: This is a clumsy hack; make it better - Width(RUN_NUMBER_WIDTH + 4) + PaddingLeft((runNumberWidth - 20) / 2). // TODO: This is a clumsy hack; make it better + Width(runNumberWidth + 4) successTextStyle = lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color(SUCCESS_COLOR)) + Foreground(lipgloss.Color(successColor)) failureTextStyle = lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color(FAILURE_COLOR)) + Foreground(lipgloss.Color(failureColor)) errorTextStyle = lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color(ERROR_COLOR)) + Foreground(lipgloss.Color(errorColor)) faintStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(CONTEXT_COLOR)) + Foreground(lipgloss.Color(contextColor)) failureHeadingStyle = nonFgStyle. Bold(true). - Foreground(lipgloss.Color(FAILURE_COLOR)) + Foreground(lipgloss.Color(failureColor)) errorHeadingStyle = nonFgStyle. Bold(true). - Foreground(lipgloss.Color(ERROR_COLOR)) + Foreground(lipgloss.Color(errorColor)) errorDetailStyle = nonFgStyle. - Foreground(lipgloss.Color(ERROR_DETAIL_COLOR)) + Foreground(lipgloss.Color(errorDetailColor)) ) diff --git a/ui/types.go b/ui/types.go index bd96332..fec7711 100644 --- a/ui/types.go +++ b/ui/types.go @@ -7,13 +7,13 @@ type Workflow struct { Repo string `yaml:"repo"` Name string `yaml:"name"` Key *string `yaml:"key"` - Url *string `yaml:"url"` + URL *string `yaml:"url"` } type WorkflowRunNodesResult struct { - Id string + ID string RunNumber int - Url string + URL string CreatedAt githubv4.DateTime CheckSuite struct { Conclusion string @@ -22,14 +22,14 @@ type WorkflowRunNodesResult struct { type WorkflowResult struct { Name string - Id string + ID string Runs struct { Nodes []WorkflowRunNodesResult } `graphql:"runs(first: $numWorkflowRuns)"` } type NodeResult struct { - Id string + ID string Workflow WorkflowResult `graphql:"... on Workflow"` } @@ -59,7 +59,7 @@ type htmlRunDetails struct { type htmlWorkflowResult struct { Details htmlRunDetails - Url string + URL string Success bool Error bool } @@ -70,10 +70,11 @@ type htmlDataRow struct { } type htmlData struct { - Title string - Columns []string - Rows []htmlDataRow - Failures map[string]string - Errors *[]error - Timestamp string + Title string + CurrentRepo *string + Columns []string + Rows []htmlDataRow + Failures map[string]string + Errors *[]error + Timestamp string } diff --git a/ui/ui.go b/ui/ui.go index 8d5e365..b9023e6 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -6,11 +6,9 @@ import ( "os" tea "github.com/charmbracelet/bubbletea" - "github.com/shurcooL/githubv4" ) -func RenderUI(ghClient *githubv4.Client, workflows []Workflow, outputFmt OutputFmt, htmlTemplate string) { - +func RenderUI(config Config) { if len(os.Getenv("DEBUG")) > 0 { f, err := tea.LogToFile("debug.log", "debug") if err != nil { @@ -21,13 +19,13 @@ func RenderUI(ghClient *githubv4.Client, workflows []Workflow, outputFmt OutputF } var opts []tea.ProgramOption - if outputFmt != UnspecifiedFmt { + if config.Fmt != UnspecifiedFmt { opts = append(opts, tea.WithoutRenderer()) // TODO: this may be a hack, and will prevent using STDIN for // CLI mode, find a better way opts = append(opts, tea.WithInput(nil)) } - p := tea.NewProgram(InitialModel(ghClient, workflows, outputFmt, htmlTemplate), opts...) + p := tea.NewProgram(InitialModel(config), opts...) if _, err := p.Run(); err != nil { log.Fatalf("Something went wrong %s", err) } diff --git a/ui/update.go b/ui/update.go index 2de5338..d583017 100644 --- a/ui/update.go +++ b/ui/update.go @@ -6,8 +6,7 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { @@ -30,18 +29,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } else { workflowRunKey = fmt.Sprintf("%s:%s", msg.workflow.Repo, msg.workflow.Name) } - m.failedWorkflowURLs[fmt.Sprintf("%s #%2d", workflowRunKey, result.RunNumber)] = result.Url + m.failedWorkflowURLs[fmt.Sprintf("%s #%2d", workflowRunKey, result.RunNumber)] = result.URL } } m.workFlowResults[msg.workflow.ID] = workflowRunResults{results: msg.query.Workflow.Runs.Nodes} } - m.numResults += 1 - if m.numResults >= len(m.workflows) { + m.numResults++ + if m.numResults >= len(m.config.Workflows) { return m, quitProg() } case quitProgMsg: if !m.outputPrinted { - switch m.outputFmt { + switch m.config.Fmt { case HTMLFmt: v := m.renderHTML() fmt.Print(v) diff --git a/ui/view.go b/ui/view.go index 67ee617..5e49c4a 100644 --- a/ui/view.go +++ b/ui/view.go @@ -2,19 +2,20 @@ package ui import ( "bytes" + _ "embed" "fmt" + "html/template" "strings" "time" - "html/template" - "github.com/charmbracelet/lipgloss" humanize "github.com/dustin/go-humanize" ) const ( - RUN_NUMBER_WIDTH = 30 - WORKFLOW_NAME_WIDTH = 30 + runNumberWidth = 30 + workflowNameWidth = 30 + runNumberPadding = 6 ) const ( @@ -22,25 +23,37 @@ const ( SystemNotFound = "not found" ) -func (m model) renderHTML() string { +var ( + //go:embed assets/template.html + htmlTemplate string + //go:embed assets/error.html + htmlErrorTemplate string +) + +func (m Model) renderHTML() string { var columns []string - var rows []htmlDataRow + rows := make([]htmlDataRow, len(m.config.Workflows)) data := htmlData{ - Title: "act3", + Title: "act3", + CurrentRepo: m.config.CurrentRepo, } columns = append(columns, "workflow") columns = append(columns, []string{"last", "2nd last", "3rd last"}...) - for _, workflow := range m.workflows { + for i, workflow := range m.config.Workflows { var workflowKey string if workflow.Key != nil { workflowKey = *workflow.Key } else { - workflowKey = fmt.Sprintf("%s:%s", workflow.Repo, workflow.Name) + if m.config.CurrentRepo != nil { + workflowKey = workflow.Name + } else { + workflowKey = fmt.Sprintf("%s:%s", workflow.Repo, workflow.Name) + } } var data []htmlWorkflowResult @@ -57,7 +70,6 @@ func (m model) renderHTML() string { Error: true, }) } - } else { for _, rr := range workflowResults.results { var resultSignifier string @@ -69,13 +81,13 @@ func (m model) renderHTML() string { resultSignifier = "❌" success = false } - var resultsDate = "(" + rr.CreatedAt.Time.Format("Jan 2") + ")" + resultsDate := "(" + rr.CreatedAt.Time.Format("Jan 2") + ")" var url string - if workflow.Url != nil { - url = strings.Replace(*workflow.Url, "{{runNumber}}", fmt.Sprintf("%d", rr.RunNumber), -1) + if workflow.URL != nil { + url = strings.Replace(*workflow.URL, "{{runNumber}}", fmt.Sprintf("%d", rr.RunNumber), -1) } else { - url = rr.Url + url = rr.URL } data = append(data, htmlWorkflowResult{ Details: htmlRunDetails{ @@ -85,16 +97,16 @@ func (m model) renderHTML() string { Context: resultsDate, }, Success: success, - Url: url, + URL: url, }, ) } } - rows = append(rows, htmlDataRow{ + rows[i] = htmlDataRow{ Key: workflowKey, Data: data, - }) + } } data.Columns = columns @@ -109,29 +121,32 @@ func (m model) renderHTML() string { var tmpl *template.Template var err error - if m.htmlTemplate == "" { - tmpl, err = template.New("act3").Parse(HTMLTemplText) + if m.config.HTMLTemplate == "" { + tmpl, err = template.New("act3").Parse(htmlTemplate) } else { - tmpl, err = template.New("act3").Parse(m.htmlTemplate) + tmpl, err = template.New("act3").Parse(m.config.HTMLTemplate) } if err != nil { - return fmt.Sprintf(string(errorTemplate), err.Error()) + return fmt.Sprintf(htmlErrorTemplate, err.Error()) } var buf bytes.Buffer err = tmpl.Execute(&buf, data) if err != nil { - return fmt.Sprintf(string(errorTemplate), err.Error()) + return fmt.Sprintf(htmlErrorTemplate, err.Error()) } return buf.String() } -func (m model) View() string { +func (m Model) View() string { var s string s += "\n" s += " " + headerStyle.Render("act3") + if m.config.CurrentRepo != nil { + s += currentRepoStyle.Render(*m.config.CurrentRepo) + } s += "\n\n" s += workflowStyle.Render("workflow") @@ -143,11 +158,17 @@ func (m model) View() string { s += "\n\n" var style lipgloss.Style - for _, workflow := range m.workflows { + for _, workflow := range m.config.Workflows { if workflow.Key != nil { - s += workflowStyle.Render(RightPadTrim(*workflow.Key, WORKFLOW_NAME_WIDTH)) + s += workflowStyle.Render(RightPadTrim(*workflow.Key, workflowNameWidth)) } else { - s += workflowStyle.Render(RightPadTrim(fmt.Sprintf("%s:%s", workflow.Repo, workflow.Name), WORKFLOW_NAME_WIDTH)) + var wf string + if m.config.CurrentRepo != nil { + wf = workflow.Name + } else { + wf = fmt.Sprintf("%s:%s", workflow.Repo, workflow.Name) + } + s += workflowStyle.Render(RightPadTrim(wf, workflowNameWidth)) } workflowResults := m.workFlowResults[workflow.ID] if workflowResults.err != nil { @@ -159,7 +180,6 @@ func (m model) View() string { )) } } else { - for _, rr := range workflowResults.results { var resultSignifier string if rr.CheckSuite.Conclusion == "SUCCESS" { @@ -169,9 +189,9 @@ func (m model) View() string { resultSignifier = "❌" style = failureTextStyle } - var resultsDate = "(" + humanize.Time(rr.CreatedAt.Time) + ")" + resultsDate := "(" + humanize.Time(rr.CreatedAt.Time) + ")" s += runResultStyle.Render(fmt.Sprintf("%s %s %s", - style.Render(fmt.Sprintf("#%2d", rr.RunNumber)), + style.Render(RightPadTrim(fmt.Sprintf("#%d", rr.RunNumber), runNumberPadding)), resultSignifier, faintStyle.Render(resultsDate), )) From d46eb0b3ce5ca90ac2c9f37062b827839a44969f Mon Sep 17 00:00:00 2001 From: Dhruv Thakur Date: Sat, 10 Aug 2024 23:16:59 +0200 Subject: [PATCH 02/10] style: format file --- cmd/config.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/config.go b/cmd/config.go index aa2a3c9..b2760f3 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -36,5 +36,4 @@ func ReadConfig(configFilePath string) ([]ui.Workflow, error) { } return config.Workflows, nil - } From c95c68944cf929625dba91d829fd0fa6b5162e04 Mon Sep 17 00:00:00 2001 From: Dhruv Thakur Date: Sat, 10 Aug 2024 23:37:37 +0200 Subject: [PATCH 03/10] feat: add -r --- cmd/root.go | 52 ++++++++++++++++++++++++++++++++-------------- docker-compose.yml | 10 +++++++++ 2 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 docker-compose.yml diff --git a/cmd/root.go b/cmd/root.go index 5270ee2..39bd154 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "time" ghapi "github.com/cli/go-gh/v2/pkg/api" @@ -21,6 +22,8 @@ const ( ) var ( + errFlagCombIncorrect = errors.New("flag combination incorrect") + errIncorrectRepoProvided = errors.New("incorrect repo provided") errCouldntGetConfigDir = errors.New("couldn't get your config directory") errConfigFilePathEmpty = errors.New("config file path is empty") errIncorrectOutputFmt = errors.New("incorrect value for output format provided") @@ -37,6 +40,7 @@ var ( format = flag.String("format", "", "output format to use; possible values: html") htmlTemplateFile = flag.String("html-template-file", "", "path of the HTML template file to use") global = flag.Bool("g", false, "whether to use workflows defined globally via the config file") + repo = flag.String("r", "", "repo to fetch worflows for, in the format \"owner/repo\"") ) func Execute() error { @@ -69,10 +73,22 @@ Let %s know about this via %s. flag.Parse() + // flag validation if *configFilePath == "" { return fmt.Errorf("%w", errConfigFilePathEmpty) } + if *global && *repo != "" { + return fmt.Errorf("%w; -g and -r cannot both be provided at the same time", errFlagCombIncorrect) + } + + if *repo != "" { + repoEls := strings.Split(*repo, "/") + if len(repoEls) != 2 { + return fmt.Errorf("%w; repo needs to be in the format \"owner/repo\"", errIncorrectRepoProvided) + } + } + var outputFmt ui.OutputFmt if *format != "" { switch *format { @@ -83,6 +99,19 @@ Let %s know about this via %s. } } + var htmlTemplate string + if *htmlTemplateFile != "" { + _, err := os.Stat(*htmlTemplateFile) + if os.IsNotExist(err) { + return fmt.Errorf("%w: path: %s", errTemplateFileDoesntExit, *htmlTemplateFile) + } + templateFileContents, err := os.ReadFile(*htmlTemplateFile) + if err != nil { + return fmt.Errorf("%w: %s", errCouldntReadTemplateFile, err.Error()) + } + htmlTemplate = string(templateFileContents) + } + clientOpts := ghapi.ClientOptions{ EnableCache: true, CacheTTL: time.Second * 30, @@ -107,9 +136,13 @@ Let %s know about this via %s. } } else { - currentRepo, err = getCurrentRepo() - if err != nil { - return err + if *repo != "" { + currentRepo = *repo + } else { + currentRepo, err = getCurrentRepo() + if err != nil { + return err + } } ghRClient, err := ghapi.NewRESTClient(clientOpts) if err != nil { @@ -126,19 +159,6 @@ Let %s know about this via %s. return fmt.Errorf("%w", errNoWorkflows) } - var htmlTemplate string - if *htmlTemplateFile != "" { - _, err := os.Stat(*htmlTemplateFile) - if os.IsNotExist(err) { - return fmt.Errorf("%w: path: %s", errTemplateFileDoesntExit, *htmlTemplateFile) - } - templateFileContents, err := os.ReadFile(*htmlTemplateFile) - if err != nil { - return fmt.Errorf("%w: %s", errCouldntReadTemplateFile, err.Error()) - } - htmlTemplate = string(templateFileContents) - } - ghClient, err := ghapi.NewGraphQLClient(clientOpts) if err != nil { return fmt.Errorf("%w: %s", errCouldntGetGHClient, err.Error()) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..80ae02a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3.8' + +# to test operations on linux +services: + act3-dev: + image: golang:1.22.5-alpine + volumes: + - .:/go/src/app + working_dir: /go/src/app + command: sleep infinity From 8884eecac62dfab6c2dfd21b54f4a5e60b8e04d1 Mon Sep 17 00:00:00 2001 From: Dhruv Thakur Date: Sun, 11 Aug 2024 10:16:12 +0200 Subject: [PATCH 04/10] fix: error out if html gen fails --- .github/workflows/build.yml | 46 ++++++++++++++++++++++-- examples/html/act3.yml | 4 +++ examples/html/template.html | 10 +++--- ui/assets/error.html | 11 ------ ui/assets/template.html | 10 +++--- ui/initial.go | 10 +++--- ui/model.go | 14 ++++---- ui/styles.go | 10 +++--- ui/types.go | 4 +++ ui/update.go | 17 ++++++--- ui/view.go | 72 ++++++++++++++++++++++--------------- 11 files changed, 134 insertions(+), 74 deletions(-) delete mode 100644 ui/assets/error.html diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a902b23..65ddad8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,6 +21,20 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + - name: go build + run: go build -v ./... + - name: go test + run: go test -v ./... + + lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: @@ -29,7 +43,35 @@ jobs: uses: golangci/golangci-lint-action@v6 with: version: v1.58 + + test-run: + name: test-run + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + - name: Generate GH token + id: generate-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.GH_TOKEN_APP_ID }} + private-key: ${{ secrets.GH_TOKEN_APP_PRIVATE_KEY }} - name: go build run: go build -v ./... - - name: go test - run: go test -v ./... + - name: Test run + run: | + ./act3 + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + - name: Generate HTML output + run: | + ./act3 \ + -g \ + -config-file=./examples/html/act3.yml \ + -format=html \ + -html-template-file=./examples/html/template.html + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} diff --git a/examples/html/act3.yml b/examples/html/act3.yml index 7e9ac6a..03dbb8e 100644 --- a/examples/html/act3.yml +++ b/examples/html/act3.yml @@ -33,6 +33,10 @@ workflows: repo: dhth/mult name: build +- id: W_kwDOMTtucc4GVkAg + repo: dhth/omm + name: build + - id: W_kwDOLafHJ84FQglT repo: dhth/outtasync name: build diff --git a/examples/html/template.html b/examples/html/template.html index 17a93df..5b5aaef 100644 --- a/examples/html/template.html +++ b/examples/html/template.html @@ -32,8 +32,8 @@

{{.Title}}

- {{if .Url}} - + {{if .URL}} +

{{.Details.NumberFormatted}}

{{.Details.Indicator}}

{{.Details.Context}}

@@ -46,8 +46,8 @@

{{.Title}}

- {{if .Url}} - + {{if .URL}} +

{{.Details.NumberFormatted}}

{{.Details.Indicator}}

{{.Details.Context}}

@@ -69,7 +69,7 @@

{{.Title}}




-

Failed Runs

+

Non Successful Runs


{{range $key, $value := .Failures -}} diff --git a/ui/assets/error.html b/ui/assets/error.html deleted file mode 100644 index 317a39a..0000000 --- a/ui/assets/error.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - -

Something went wrong generating the HTML

-

Error: %s

- - diff --git a/ui/assets/template.html b/ui/assets/template.html index d3e0f93..5d0f172 100644 --- a/ui/assets/template.html +++ b/ui/assets/template.html @@ -34,8 +34,8 @@

{{.Title}}

{{else if .Success}} {{else}}
- {{if .Url}} - + {{if .URL}} +

{{.Details.NumberFormatted}}

{{.Details.Indicator}}

{{.Details.Context}}

@@ -48,8 +48,8 @@

{{.Title}}

- {{if .Url}} - + {{if .URL}} +

{{.Details.NumberFormatted}}

{{.Details.Indicator}}

{{.Details.Context}}

@@ -71,7 +71,7 @@

{{.Title}}




-

Failed Runs

+

Non Successful Runs


{{range $key, $value := .Failures -}} diff --git a/ui/initial.go b/ui/initial.go index 99444ea..49d2c22 100644 --- a/ui/initial.go +++ b/ui/initial.go @@ -7,11 +7,11 @@ func InitialModel(config Config) Model { failedWorkflowRunURLs := make(map[string]string) m := Model{ - config: config, - workFlowResults: workflowResults, - message: "hello", - errors: errors, - failedWorkflowURLs: failedWorkflowRunURLs, + config: config, + workFlowResults: workflowResults, + message: "hello", + errors: errors, + nonSuccessWorkflowURLs: failedWorkflowRunURLs, } return m } diff --git a/ui/model.go b/ui/model.go index 55016bb..a442075 100644 --- a/ui/model.go +++ b/ui/model.go @@ -5,13 +5,13 @@ import ( ) type Model struct { - config Config - workFlowResults map[string]workflowRunResults - numResults int - message string - errors []error - failedWorkflowURLs map[string]string - outputPrinted bool + config Config + workFlowResults map[string]workflowRunResults + numResults int + message string + errors []error + nonSuccessWorkflowURLs map[string]string + outputPrinted bool } func (m Model) Init() tea.Cmd { diff --git a/ui/styles.go b/ui/styles.go index 37906e1..fa66e55 100644 --- a/ui/styles.go +++ b/ui/styles.go @@ -8,7 +8,7 @@ const ( runNumberColor = "#83a598" workflowColor = "#d3869b" successColor = "#b8bb26" - failureColor = "#fb4934" + nonSuccessColor = "#fb4934" errorColor = "#fabd2f" errorDetailColor = "#665c54" contextColor = "#665c54" @@ -55,9 +55,9 @@ var ( Bold(true). Foreground(lipgloss.Color(successColor)) - failureTextStyle = lipgloss.NewStyle(). + nonSuccessTextStyle = lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color(failureColor)) + Foreground(lipgloss.Color(nonSuccessColor)) errorTextStyle = lipgloss.NewStyle(). Bold(true). @@ -66,9 +66,9 @@ var ( faintStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color(contextColor)) - failureHeadingStyle = nonFgStyle. + nonSuccessHeadingStyle = nonFgStyle. Bold(true). - Foreground(lipgloss.Color(failureColor)) + Foreground(lipgloss.Color(nonSuccessColor)) errorHeadingStyle = nonFgStyle. Bold(true). diff --git a/ui/types.go b/ui/types.go index fec7711..1cadf04 100644 --- a/ui/types.go +++ b/ui/types.go @@ -2,6 +2,10 @@ package ui import "github.com/shurcooL/githubv4" +const ( + checkSuiteSuccess = "SUCCESS" +) + type Workflow struct { ID string `yaml:"id"` Repo string `yaml:"repo"` diff --git a/ui/update.go b/ui/update.go index d583017..a5919a5 100644 --- a/ui/update.go +++ b/ui/update.go @@ -2,6 +2,7 @@ package ui import ( "fmt" + "os" tea "github.com/charmbracelet/bubbletea" ) @@ -22,14 +23,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } else { m.workFlowResults[msg.workflow.ID] = workflowRunResults{results: msg.query.Workflow.Runs.Nodes, err: msg.err, errorIndex: len(m.errors)} for _, result := range msg.query.NodeResult.Workflow.Runs.Nodes { - if result.CheckSuite.Conclusion == "FAILURE" { + if result.CheckSuite.Conclusion != checkSuiteSuccess { + indicator := getCheckSuiteIndicator(result.CheckSuite.Conclusion) var workflowRunKey string if msg.workflow.Key != nil { - workflowRunKey = *msg.workflow.Key + workflowRunKey = fmt.Sprintf("%s %s", *msg.workflow.Key, indicator) } else { - workflowRunKey = fmt.Sprintf("%s:%s", msg.workflow.Repo, msg.workflow.Name) + workflowRunKey = fmt.Sprintf("%s: %s", msg.workflow.Repo, msg.workflow.Name) } - m.failedWorkflowURLs[fmt.Sprintf("%s #%2d", workflowRunKey, result.RunNumber)] = result.URL + m.nonSuccessWorkflowURLs[fmt.Sprintf("%s #%2d", workflowRunKey, result.RunNumber)] = result.URL } } m.workFlowResults[msg.workflow.ID] = workflowRunResults{results: msg.query.Workflow.Runs.Nodes} @@ -42,7 +44,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if !m.outputPrinted { switch m.config.Fmt { case HTMLFmt: - v := m.renderHTML() + v, err := m.renderHTML() + // TODO: move this out to main + if err != nil { + fmt.Fprintf(os.Stderr, "Something went wrong generating HTML output.\nError: %s\n", err.Error()) + os.Exit(1) + } fmt.Print(v) m.outputPrinted = true } diff --git a/ui/view.go b/ui/view.go index 5e49c4a..afddb72 100644 --- a/ui/view.go +++ b/ui/view.go @@ -13,9 +13,9 @@ import ( ) const ( - runNumberWidth = 30 + runNumberWidth = 40 workflowNameWidth = 30 - runNumberPadding = 6 + runNumberPadding = 8 ) const ( @@ -23,15 +23,10 @@ const ( SystemNotFound = "not found" ) -var ( - //go:embed assets/template.html - htmlTemplate string +//go:embed assets/template.html +var htmlTemplate string - //go:embed assets/error.html - htmlErrorTemplate string -) - -func (m Model) renderHTML() string { +func (m Model) renderHTML() (string, error) { var columns []string rows := make([]htmlDataRow, len(m.config.Workflows)) @@ -73,13 +68,10 @@ func (m Model) renderHTML() string { } else { for _, rr := range workflowResults.results { var resultSignifier string - var success bool - if rr.CheckSuite.Conclusion == "SUCCESS" { - resultSignifier = "✅" + success := false + resultSignifier = getCheckSuiteIndicator(rr.CheckSuite.Conclusion) + if rr.CheckSuite.Conclusion == checkSuiteSuccess { success = true - } else { - resultSignifier = "❌" - success = false } resultsDate := "(" + rr.CreatedAt.Time.Format("Jan 2") + ")" @@ -114,8 +106,8 @@ func (m Model) renderHTML() string { if len(m.errors) > 0 { data.Errors = &m.errors } - if len(m.failedWorkflowURLs) > 0 { - data.Failures = m.failedWorkflowURLs + if len(m.nonSuccessWorkflowURLs) > 0 { + data.Failures = m.nonSuccessWorkflowURLs } data.Timestamp = time.Now().Format("2006-01-02 15:04:05 MST") @@ -127,16 +119,16 @@ func (m Model) renderHTML() string { tmpl, err = template.New("act3").Parse(m.config.HTMLTemplate) } if err != nil { - return fmt.Sprintf(htmlErrorTemplate, err.Error()) + return "", err } var buf bytes.Buffer err = tmpl.Execute(&buf, data) if err != nil { - return fmt.Sprintf(htmlErrorTemplate, err.Error()) + return "", err } - return buf.String() + return buf.String(), nil } func (m Model) View() string { @@ -182,13 +174,12 @@ func (m Model) View() string { } else { for _, rr := range workflowResults.results { var resultSignifier string - if rr.CheckSuite.Conclusion == "SUCCESS" { - resultSignifier = "✅" + style = nonSuccessTextStyle + resultSignifier = getCheckSuiteIndicator(rr.CheckSuite.Conclusion) + if rr.CheckSuite.Conclusion == checkSuiteSuccess { style = successTextStyle - } else { - resultSignifier = "❌" - style = failureTextStyle } + resultsDate := "(" + humanize.Time(rr.CreatedAt.Time) + ")" s += runResultStyle.Render(fmt.Sprintf("%s %s %s", style.Render(RightPadTrim(fmt.Sprintf("#%d", rr.RunNumber), runNumberPadding)), @@ -200,11 +191,11 @@ func (m Model) View() string { s += "\n" } - if len(m.failedWorkflowURLs) > 0 { + if len(m.nonSuccessWorkflowURLs) > 0 { s += "\n" - s += failureHeadingStyle.Render("Failed runs") + s += nonSuccessHeadingStyle.Render("Non successful runs") s += "\n" - for k, v := range m.failedWorkflowURLs { + for k, v := range m.nonSuccessWorkflowURLs { s += errorDetailStyle.Render(fmt.Sprintf("%s%s", RightPadTrim(k, 65), v)) s += "\n" } @@ -221,3 +212,26 @@ func (m Model) View() string { } return s } + +func getCheckSuiteIndicator(conclusion string) string { + switch conclusion { + case "ACTION_REQUIRED": + return "🔄" + case "TIMED_OUT": + return "⏰" + case "CANCELLED": + return "🚫" + case "FAILURE": + return "❌" + case "SUCCESS": + return "✅" + case "NEUTRAL": + return "😐" + case "SKIPPED": + return "⏭️" + case "STARTUP_FAILURE": + return "🛑" + default: + return "🟡" + } +} From b57f3b9caa1eb14923bbbb77c3f7cf223ce4090a Mon Sep 17 00:00:00 2001 From: Dhruv Thakur Date: Sun, 11 Aug 2024 10:18:52 +0200 Subject: [PATCH 05/10] fix: broken action --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 65ddad8..088d07c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,7 +60,7 @@ jobs: app-id: ${{ vars.GH_TOKEN_APP_ID }} private-key: ${{ secrets.GH_TOKEN_APP_PRIVATE_KEY }} - name: go build - run: go build -v ./... + run: go build . - name: Test run run: | ./act3 From 86feaef6722a4c91220beeabb21ee1e1fd22717e Mon Sep 17 00:00:00 2001 From: Dhruv Thakur Date: Sun, 11 Aug 2024 10:21:13 +0200 Subject: [PATCH 06/10] fix: remove test run --- .github/workflows/build.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 088d07c..97b53bc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,11 +61,6 @@ jobs: private-key: ${{ secrets.GH_TOKEN_APP_PRIVATE_KEY }} - name: go build run: go build . - - name: Test run - run: | - ./act3 - env: - GH_TOKEN: ${{ steps.generate-token.outputs.token }} - name: Generate HTML output run: | ./act3 \ From bdd9596531e0af6e59658beef7fd812adefcb36e Mon Sep 17 00:00:00 2001 From: Dhruv Thakur Date: Sat, 17 Aug 2024 11:33:13 +0200 Subject: [PATCH 07/10] ci: add matrix build strategy --- .github/workflows/build.yml | 30 ++++++++++-------------------- .github/workflows/lint.yml | 31 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 97b53bc..3e46b70 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,10 @@ env: jobs: build: name: build - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up Go @@ -30,42 +33,29 @@ jobs: - name: go test run: go test -v ./... - lint: - name: lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - - name: golangci-lint - uses: golangci/golangci-lint-action@v6 - with: - version: v1.58 - test-run: name: test-run - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} + - name: go build + run: go build . - name: Generate GH token id: generate-token uses: actions/create-github-app-token@v1 with: app-id: ${{ vars.GH_TOKEN_APP_ID }} private-key: ${{ secrets.GH_TOKEN_APP_PRIVATE_KEY }} - - name: go build - run: go build . - name: Generate HTML output run: | ./act3 \ - -g \ - -config-file=./examples/html/act3.yml \ -format=html \ -html-template-file=./examples/html/template.html env: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..2f2edf3 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,31 @@ +name: lint + +on: + push: + branches: [ "main" ] + pull_request: + paths: + - "go.*" + - "**/*.go" + - ".github/workflows/*.yml" + +permissions: + contents: read + +env: + GO_VERSION: '1.22.5' + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.58 From c82033fc3bd01703e2182f5e43c387133da286f2 Mon Sep 17 00:00:00 2001 From: Dhruv Thakur Date: Sat, 17 Aug 2024 12:13:11 +0200 Subject: [PATCH 08/10] fix: show URLs only for failed runs, +refactor --- cmd/config.go | 6 +- cmd/current.go | 7 +- cmd/root.go | 5 +- internal/gh/gh.go | 60 +++++++++++++++++ {ui => internal/ui}/assets/template.html | 0 {ui => internal/ui}/cmds.go | 5 +- {ui => internal/ui}/config.go | 7 +- {ui => internal/ui}/initial.go | 0 {ui => internal/ui}/model.go | 0 {ui => internal/ui}/msgs.go | 6 +- {ui => internal/ui}/styles.go | 0 internal/ui/types.go | 45 +++++++++++++ {ui => internal/ui}/ui.go | 0 {ui => internal/ui}/update.go | 10 +-- internal/ui/utils.go | 50 ++++++++++++++ {ui => internal/ui}/view.go | 28 +------- ui/types.go | 84 ------------------------ ui/utils.go | 25 ------- 18 files changed, 184 insertions(+), 154 deletions(-) rename {ui => internal/ui}/assets/template.html (100%) rename {ui => internal/ui}/cmds.go (77%) rename {ui => internal/ui}/config.go (54%) rename {ui => internal/ui}/initial.go (100%) rename {ui => internal/ui}/model.go (100%) rename {ui => internal/ui}/msgs.go (51%) rename {ui => internal/ui}/styles.go (100%) create mode 100644 internal/ui/types.go rename {ui => internal/ui}/ui.go (100%) rename {ui => internal/ui}/update.go (80%) create mode 100644 internal/ui/utils.go rename {ui => internal/ui}/view.go (90%) delete mode 100644 ui/types.go delete mode 100644 ui/utils.go diff --git a/cmd/config.go b/cmd/config.go index b2760f3..07214bd 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -5,12 +5,12 @@ import ( "os/user" "strings" - "github.com/dhth/act3/ui" + "github.com/dhth/act3/internal/gh" "gopkg.in/yaml.v3" ) type Config struct { - Workflows []ui.Workflow `yaml:"workflows"` + Workflows []gh.Workflow `yaml:"workflows"` } func expandTilde(path string) string { @@ -24,7 +24,7 @@ func expandTilde(path string) string { return path } -func ReadConfig(configFilePath string) ([]ui.Workflow, error) { +func ReadConfig(configFilePath string) ([]gh.Workflow, error) { localFile, err := os.ReadFile(expandTilde(configFilePath)) if err != nil { return nil, err diff --git a/cmd/current.go b/cmd/current.go index 2a1b6ea..d8afab7 100644 --- a/cmd/current.go +++ b/cmd/current.go @@ -9,7 +9,6 @@ import ( ghapi "github.com/cli/go-gh/v2/pkg/api" "github.com/dhth/act3/internal/gh" - "github.com/dhth/act3/ui" "github.com/go-git/go-git/v5" ) @@ -21,15 +20,15 @@ var ( errCouldntParseRemoteURL = errors.New("couldn't parse remote URL") ) -func getWorkflowsForCurrentRepo(ghClient *ghapi.RESTClient, repo string) ([]ui.Workflow, error) { +func getWorkflowsForCurrentRepo(ghClient *ghapi.RESTClient, repo string) ([]gh.Workflow, error) { wd, err := gh.GetWorkflowDetails(ghClient, repo) if err != nil { return nil, err } - workflows := make([]ui.Workflow, len(wd.Workflows)) + workflows := make([]gh.Workflow, len(wd.Workflows)) for i, w := range wd.Workflows { - workflows[i] = ui.Workflow{ + workflows[i] = gh.Workflow{ ID: w.NodeID, Repo: repo, Name: w.Name, diff --git a/cmd/root.go b/cmd/root.go index 39bd154..169b891 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,7 +11,8 @@ import ( "time" ghapi "github.com/cli/go-gh/v2/pkg/api" - "github.com/dhth/act3/ui" + "github.com/dhth/act3/internal/gh" + "github.com/dhth/act3/internal/ui" ) const ( @@ -117,7 +118,7 @@ Let %s know about this via %s. CacheTTL: time.Second * 30, Timeout: 8 * time.Second, } - var workflows []ui.Workflow + var workflows []gh.Workflow var currentRepo string var err error diff --git a/internal/gh/gh.go b/internal/gh/gh.go index 9ea4585..7eeea7b 100644 --- a/internal/gh/gh.go +++ b/internal/gh/gh.go @@ -4,8 +4,59 @@ import ( "fmt" ghapi "github.com/cli/go-gh/v2/pkg/api" + "github.com/shurcooL/githubv4" ) +// cs = check suite +// https://docs.github.com/en/graphql/reference/enums#checkconclusionstate +const ( + CSConclusionActionReq = "ACTION_REQUIRED" + CSConclusionCancelled = "CANCELLED" + CSConclusionFailure = "FAILURE" + CSConclusionNeutral = "NEUTRAL" + CSConclusionSkipped = "SKIPPED" + CSConclusionStartupFailure = "STARTUP_FAILURE" + CSConclusionSuccess = "SUCCESS" + CSConclusionTimedOut = "TIMED_OUT" +) + +type Workflow struct { + ID string `yaml:"id"` + Repo string `yaml:"repo"` + Name string `yaml:"name"` + Key *string `yaml:"key"` + URL *string `yaml:"url"` +} + +type CheckSuite struct { + Conclusion string +} + +type WorkflowRunNodesResult struct { + ID string + RunNumber int + URL string + CreatedAt githubv4.DateTime + CheckSuite CheckSuite +} + +type WorkflowResult struct { + Name string + ID string + Runs struct { + Nodes []WorkflowRunNodesResult + } `graphql:"runs(first: $numWorkflowRuns)"` +} + +type NodeResult struct { + ID string + Workflow WorkflowResult `graphql:"... on Workflow"` +} + +type QueryResult struct { + NodeResult `graphql:"node(id: $workflowId)"` +} + type WorkflowDetailsResult struct { NodeID string `json:"node_id"` Name string @@ -23,3 +74,12 @@ func GetWorkflowDetails(ghClient *ghapi.RESTClient, repo string) (WorkflowDetail err := ghClient.Get(fmt.Sprintf("repos/%s/actions/workflows", repo), &wd) return wd, err } + +func (cs CheckSuite) IsAFailure() bool { + switch cs.Conclusion { + case CSConclusionActionReq, CSConclusionTimedOut, CSConclusionFailure, CSConclusionStartupFailure: + return true + default: + return false + } +} diff --git a/ui/assets/template.html b/internal/ui/assets/template.html similarity index 100% rename from ui/assets/template.html rename to internal/ui/assets/template.html diff --git a/ui/cmds.go b/internal/ui/cmds.go similarity index 77% rename from ui/cmds.go rename to internal/ui/cmds.go index 6e3422b..6b01d0b 100644 --- a/ui/cmds.go +++ b/internal/ui/cmds.go @@ -4,15 +4,16 @@ import ( tea "github.com/charmbracelet/bubbletea" ghapi "github.com/cli/go-gh/v2/pkg/api" ghgql "github.com/cli/shurcooL-graphql" + "github.com/dhth/act3/internal/gh" ) -func getWorkflowRuns(ghClient *ghapi.GraphQLClient, workflow Workflow) tea.Cmd { +func getWorkflowRuns(ghClient *ghapi.GraphQLClient, workflow gh.Workflow) tea.Cmd { return func() tea.Msg { variables := map[string]interface{}{ "numWorkflowRuns": ghgql.Int(3), "workflowId": ghgql.ID(workflow.ID), } - var query QueryResult + var query gh.QueryResult err := ghClient.Query("GetWorkflows", &query, variables) return WorkflowRunsFetchedMsg{workflow, query, err} diff --git a/ui/config.go b/internal/ui/config.go similarity index 54% rename from ui/config.go rename to internal/ui/config.go index 026e806..5e52875 100644 --- a/ui/config.go +++ b/internal/ui/config.go @@ -1,10 +1,13 @@ package ui -import ghapi "github.com/cli/go-gh/v2/pkg/api" +import ( + ghapi "github.com/cli/go-gh/v2/pkg/api" + "github.com/dhth/act3/internal/gh" +) type Config struct { GHClient *ghapi.GraphQLClient - Workflows []Workflow + Workflows []gh.Workflow CurrentRepo *string Fmt OutputFmt HTMLTemplate string diff --git a/ui/initial.go b/internal/ui/initial.go similarity index 100% rename from ui/initial.go rename to internal/ui/initial.go diff --git a/ui/model.go b/internal/ui/model.go similarity index 100% rename from ui/model.go rename to internal/ui/model.go diff --git a/ui/msgs.go b/internal/ui/msgs.go similarity index 51% rename from ui/msgs.go rename to internal/ui/msgs.go index 7adedcc..1cb15de 100644 --- a/ui/msgs.go +++ b/internal/ui/msgs.go @@ -1,8 +1,10 @@ package ui +import "github.com/dhth/act3/internal/gh" + type WorkflowRunsFetchedMsg struct { - workflow Workflow - query QueryResult + workflow gh.Workflow + query gh.QueryResult err error } diff --git a/ui/styles.go b/internal/ui/styles.go similarity index 100% rename from ui/styles.go rename to internal/ui/styles.go diff --git a/internal/ui/types.go b/internal/ui/types.go new file mode 100644 index 0000000..026dcf8 --- /dev/null +++ b/internal/ui/types.go @@ -0,0 +1,45 @@ +package ui + +import "github.com/dhth/act3/internal/gh" + +type OutputFmt uint + +const ( + UnspecifiedFmt OutputFmt = iota + HTMLFmt +) + +type workflowRunResults struct { + results []gh.WorkflowRunNodesResult + err error + errorIndex int +} + +type htmlRunDetails struct { + NumberFormatted string + RunNumber string + Indicator string + Context string +} + +type htmlWorkflowResult struct { + Details htmlRunDetails + URL string + Success bool + Error bool +} + +type htmlDataRow struct { + Key string + Data []htmlWorkflowResult +} + +type htmlData struct { + Title string + CurrentRepo *string + Columns []string + Rows []htmlDataRow + Failures map[string]string + Errors *[]error + Timestamp string +} diff --git a/ui/ui.go b/internal/ui/ui.go similarity index 100% rename from ui/ui.go rename to internal/ui/ui.go diff --git a/ui/update.go b/internal/ui/update.go similarity index 80% rename from ui/update.go rename to internal/ui/update.go index a5919a5..3f48609 100644 --- a/ui/update.go +++ b/internal/ui/update.go @@ -23,15 +23,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } else { m.workFlowResults[msg.workflow.ID] = workflowRunResults{results: msg.query.Workflow.Runs.Nodes, err: msg.err, errorIndex: len(m.errors)} for _, result := range msg.query.NodeResult.Workflow.Runs.Nodes { - if result.CheckSuite.Conclusion != checkSuiteSuccess { + if result.CheckSuite.IsAFailure() { indicator := getCheckSuiteIndicator(result.CheckSuite.Conclusion) - var workflowRunKey string + var failedWorkflowRunKey string if msg.workflow.Key != nil { - workflowRunKey = fmt.Sprintf("%s %s", *msg.workflow.Key, indicator) + failedWorkflowRunKey = fmt.Sprintf("%s %s", *msg.workflow.Key, indicator) } else { - workflowRunKey = fmt.Sprintf("%s: %s", msg.workflow.Repo, msg.workflow.Name) + failedWorkflowRunKey = fmt.Sprintf("%s: %s", msg.workflow.Repo, msg.workflow.Name) } - m.nonSuccessWorkflowURLs[fmt.Sprintf("%s #%2d", workflowRunKey, result.RunNumber)] = result.URL + m.nonSuccessWorkflowURLs[fmt.Sprintf("%s #%2d", failedWorkflowRunKey, result.RunNumber)] = result.URL } } m.workFlowResults[msg.workflow.ID] = workflowRunResults{results: msg.query.Workflow.Runs.Nodes} diff --git a/internal/ui/utils.go b/internal/ui/utils.go new file mode 100644 index 0000000..87687ad --- /dev/null +++ b/internal/ui/utils.go @@ -0,0 +1,50 @@ +package ui + +import ( + "strings" + + "github.com/dhth/act3/internal/gh" +) + +func RightPadTrim(s string, length int) string { + if len(s) >= length { + if length > 3 { + return s[:length-3] + "..." + } + return s[:length] + } + return s + strings.Repeat(" ", length-len(s)) +} + +func Trim(s string, length int) string { + if len(s) >= length { + if length > 3 { + return s[:length-3] + "..." + } + return s[:length] + } + return s +} + +func getCheckSuiteIndicator(conclusion string) string { + switch conclusion { + case gh.CSConclusionActionReq: + return "🔄" + case gh.CSConclusionTimedOut: + return "⏰" + case gh.CSConclusionCancelled: + return "🚫" + case gh.CSConclusionFailure: + return "❌" + case gh.CSConclusionSuccess: + return "✅" + case gh.CSConclusionNeutral: + return "😐" + case gh.CSConclusionSkipped: + return "⏭️" + case gh.CSConclusionStartupFailure: + return "🛑" + default: + return "🟡" + } +} diff --git a/ui/view.go b/internal/ui/view.go similarity index 90% rename from ui/view.go rename to internal/ui/view.go index afddb72..a4d435c 100644 --- a/ui/view.go +++ b/internal/ui/view.go @@ -9,6 +9,7 @@ import ( "time" "github.com/charmbracelet/lipgloss" + "github.com/dhth/act3/internal/gh" humanize "github.com/dustin/go-humanize" ) @@ -70,7 +71,7 @@ func (m Model) renderHTML() (string, error) { var resultSignifier string success := false resultSignifier = getCheckSuiteIndicator(rr.CheckSuite.Conclusion) - if rr.CheckSuite.Conclusion == checkSuiteSuccess { + if rr.CheckSuite.Conclusion == gh.CSConclusionSuccess { success = true } resultsDate := "(" + rr.CreatedAt.Time.Format("Jan 2") + ")" @@ -176,7 +177,7 @@ func (m Model) View() string { var resultSignifier string style = nonSuccessTextStyle resultSignifier = getCheckSuiteIndicator(rr.CheckSuite.Conclusion) - if rr.CheckSuite.Conclusion == checkSuiteSuccess { + if rr.CheckSuite.Conclusion == gh.CSConclusionSuccess { style = successTextStyle } @@ -212,26 +213,3 @@ func (m Model) View() string { } return s } - -func getCheckSuiteIndicator(conclusion string) string { - switch conclusion { - case "ACTION_REQUIRED": - return "🔄" - case "TIMED_OUT": - return "⏰" - case "CANCELLED": - return "🚫" - case "FAILURE": - return "❌" - case "SUCCESS": - return "✅" - case "NEUTRAL": - return "😐" - case "SKIPPED": - return "⏭️" - case "STARTUP_FAILURE": - return "🛑" - default: - return "🟡" - } -} diff --git a/ui/types.go b/ui/types.go deleted file mode 100644 index 1cadf04..0000000 --- a/ui/types.go +++ /dev/null @@ -1,84 +0,0 @@ -package ui - -import "github.com/shurcooL/githubv4" - -const ( - checkSuiteSuccess = "SUCCESS" -) - -type Workflow struct { - ID string `yaml:"id"` - Repo string `yaml:"repo"` - Name string `yaml:"name"` - Key *string `yaml:"key"` - URL *string `yaml:"url"` -} - -type WorkflowRunNodesResult struct { - ID string - RunNumber int - URL string - CreatedAt githubv4.DateTime - CheckSuite struct { - Conclusion string - } -} - -type WorkflowResult struct { - Name string - ID string - Runs struct { - Nodes []WorkflowRunNodesResult - } `graphql:"runs(first: $numWorkflowRuns)"` -} - -type NodeResult struct { - ID string - Workflow WorkflowResult `graphql:"... on Workflow"` -} - -type QueryResult struct { - NodeResult `graphql:"node(id: $workflowId)"` -} - -type OutputFmt uint - -const ( - UnspecifiedFmt OutputFmt = iota - HTMLFmt -) - -type workflowRunResults struct { - results []WorkflowRunNodesResult - err error - errorIndex int -} - -type htmlRunDetails struct { - NumberFormatted string - RunNumber string - Indicator string - Context string -} - -type htmlWorkflowResult struct { - Details htmlRunDetails - URL string - Success bool - Error bool -} - -type htmlDataRow struct { - Key string - Data []htmlWorkflowResult -} - -type htmlData struct { - Title string - CurrentRepo *string - Columns []string - Rows []htmlDataRow - Failures map[string]string - Errors *[]error - Timestamp string -} diff --git a/ui/utils.go b/ui/utils.go deleted file mode 100644 index 27584d1..0000000 --- a/ui/utils.go +++ /dev/null @@ -1,25 +0,0 @@ -package ui - -import ( - "strings" -) - -func RightPadTrim(s string, length int) string { - if len(s) >= length { - if length > 3 { - return s[:length-3] + "..." - } - return s[:length] - } - return s + strings.Repeat(" ", length-len(s)) -} - -func Trim(s string, length int) string { - if len(s) >= length { - if length > 3 { - return s[:length-3] + "..." - } - return s[:length] - } - return s -} From 7d0c985504f6fdd6e0de073be3c3e6bce0a3972f Mon Sep 17 00:00:00 2001 From: Dhruv Thakur Date: Sat, 17 Aug 2024 12:42:31 +0200 Subject: [PATCH 09/10] refactor!: shorten flags --- .github/workflows/build.yml | 4 ++-- cmd/root.go | 8 ++++---- internal/ui/styles.go | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e46b70..6d5d166 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,7 +56,7 @@ jobs: - name: Generate HTML output run: | ./act3 \ - -format=html \ - -html-template-file=./examples/html/template.html + -f html \ + -t ./examples/html/template.html env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} diff --git a/cmd/root.go b/cmd/root.go index 169b891..a058278 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -38,8 +38,8 @@ var ( ) var ( - format = flag.String("format", "", "output format to use; possible values: html") - htmlTemplateFile = flag.String("html-template-file", "", "path of the HTML template file to use") + format = flag.String("f", "", "output format to use; possible values: html") + htmlTemplateFile = flag.String("t", "", "path of the HTML template file to use") global = flag.Bool("g", false, "whether to use workflows defined globally via the config file") repo = flag.String("r", "", "repo to fetch worflows for, in the format \"owner/repo\"") ) @@ -59,13 +59,13 @@ func Execute() error { } if configErr != nil { fmt.Printf(`Couldn't get your default config directory. This is a fatal error; -use --config-file to specify config file path manually. +use -c to specify config file path manually. Let %s know about this via %s. `, author, issuesURL) return fmt.Errorf("%w: %s", errCouldntGetConfigDir, configErr.Error()) } defaultConfigFilePath := filepath.Join(defaultConfigDir, configPath) - configFilePath := flag.String("config-file", defaultConfigFilePath, "path of the config file") + configFilePath := flag.String("c", defaultConfigFilePath, "path of the config file") flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n\nFlags:\n", helpText) diff --git a/internal/ui/styles.go b/internal/ui/styles.go index fa66e55..2a7dc7c 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles.go @@ -48,7 +48,6 @@ var ( Width(workflowNameWidth + 4) runResultStyle = nonFgStyle. - PaddingLeft((runNumberWidth - 20) / 2). // TODO: This is a clumsy hack; make it better Width(runNumberWidth + 4) successTextStyle = lipgloss.NewStyle(). From 0a48332279757e7507027e129af0768dfd2946f2 Mon Sep 17 00:00:00 2001 From: Dhruv Thakur Date: Sat, 17 Aug 2024 13:51:26 +0200 Subject: [PATCH 10/10] feat: highlight runs based on conclusion --- internal/ui/assets/template.html | 22 +++------------ internal/ui/styles.go | 46 ++++++++++++++++++++++++-------- internal/ui/types.go | 10 ++++--- internal/ui/view.go | 20 ++++++-------- 4 files changed, 53 insertions(+), 45 deletions(-) diff --git a/internal/ui/assets/template.html b/internal/ui/assets/template.html index 5d0f172..4781084 100644 --- a/internal/ui/assets/template.html +++ b/internal/ui/assets/template.html @@ -32,31 +32,17 @@

{{.Title}}

{{.Details.Indicator}}

{{.Details.Context}}

- {{else if .Success}} -
{{else}} diff --git a/internal/ui/styles.go b/internal/ui/styles.go index 2a7dc7c..ae691f8 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles.go @@ -1,14 +1,21 @@ package ui -import "github.com/charmbracelet/lipgloss" +import ( + "github.com/charmbracelet/lipgloss" + "github.com/dhth/act3/internal/gh" +) const ( bgColor = "#282828" headerColor = "#fe8019" runNumberColor = "#83a598" workflowColor = "#d3869b" - successColor = "#b8bb26" - nonSuccessColor = "#fb4934" + csRunningColor = "#fabd2f" + csActionReqColor = "#83a598" + csCancelledColor = "#fb4934" + csFailureColor = "#fb4934" + csSuccessColor = "#b8bb26" + csDefaultColor = "#928374" errorColor = "#fabd2f" errorDetailColor = "#665c54" contextColor = "#665c54" @@ -50,13 +57,30 @@ var ( runResultStyle = nonFgStyle. Width(runNumberWidth + 4) - successTextStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(lipgloss.Color(successColor)) - - nonSuccessTextStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(lipgloss.Color(nonSuccessColor)) + getCheckRunColor = func(checkSuiteConclusion string) string { + var color string + switch checkSuiteConclusion { + case gh.CSConclusionActionReq: + color = csActionReqColor + case gh.CSConclusionFailure, gh.CSConclusionStartupFailure: + color = csFailureColor + case gh.CSConclusionSuccess: + color = csSuccessColor + case "": + color = errorColor + default: + color = csDefaultColor + } + + return color + } + + getResultStyle = func(checkSuiteConclusion string) lipgloss.Style { + color := getCheckRunColor(checkSuiteConclusion) + return lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color(color)) + } errorTextStyle = lipgloss.NewStyle(). Bold(true). @@ -67,7 +91,7 @@ var ( nonSuccessHeadingStyle = nonFgStyle. Bold(true). - Foreground(lipgloss.Color(nonSuccessColor)) + Foreground(lipgloss.Color(csFailureColor)) errorHeadingStyle = nonFgStyle. Bold(true). diff --git a/internal/ui/types.go b/internal/ui/types.go index 026dcf8..749f045 100644 --- a/internal/ui/types.go +++ b/internal/ui/types.go @@ -23,10 +23,12 @@ type htmlRunDetails struct { } type htmlWorkflowResult struct { - Details htmlRunDetails - URL string - Success bool - Error bool + Details htmlRunDetails + URL string + Success bool + Color string + Conclusion string + Error bool } type htmlDataRow struct { diff --git a/internal/ui/view.go b/internal/ui/view.go index a4d435c..d374b87 100644 --- a/internal/ui/view.go +++ b/internal/ui/view.go @@ -9,12 +9,11 @@ import ( "time" "github.com/charmbracelet/lipgloss" - "github.com/dhth/act3/internal/gh" humanize "github.com/dustin/go-humanize" ) const ( - runNumberWidth = 40 + runNumberWidth = 36 workflowNameWidth = 30 runNumberPadding = 8 ) @@ -64,16 +63,14 @@ func (m Model) renderHTML() (string, error) { }, Success: false, Error: true, + Color: errorColor, }) } } else { for _, rr := range workflowResults.results { var resultSignifier string - success := false + success := !rr.CheckSuite.IsAFailure() resultSignifier = getCheckSuiteIndicator(rr.CheckSuite.Conclusion) - if rr.CheckSuite.Conclusion == gh.CSConclusionSuccess { - success = true - } resultsDate := "(" + rr.CreatedAt.Time.Format("Jan 2") + ")" var url string @@ -89,8 +86,10 @@ func (m Model) renderHTML() (string, error) { Indicator: resultSignifier, Context: resultsDate, }, - Success: success, - URL: url, + Success: success, + URL: url, + Conclusion: rr.CheckSuite.Conclusion, + Color: getCheckRunColor(rr.CheckSuite.Conclusion), }, ) @@ -175,11 +174,8 @@ func (m Model) View() string { } else { for _, rr := range workflowResults.results { var resultSignifier string - style = nonSuccessTextStyle + style = getResultStyle(rr.CheckSuite.Conclusion) resultSignifier = getCheckSuiteIndicator(rr.CheckSuite.Conclusion) - if rr.CheckSuite.Conclusion == gh.CSConclusionSuccess { - style = successTextStyle - } resultsDate := "(" + humanize.Time(rr.CreatedAt.Time) + ")" s += runResultStyle.Render(fmt.Sprintf("%s %s %s",
- {{if .URL}} - -

{{.Details.NumberFormatted}}

-

{{.Details.Indicator}}

-

{{.Details.Context}}

-
- {{else}} -

{{.Details.NumberFormatted}}

-

{{.Details.Indicator}}

-

{{.Details.Context}}

- {{end}} -
{{if .URL}} -

{{.Details.NumberFormatted}}

-

{{.Details.Indicator}}

+

{{.Details.NumberFormatted}}

+

{{.Details.Indicator}}

{{.Details.Context}}

{{else}} -

{{.Details.NumberFormatted}}

-

{{.Details.Indicator}}

+

{{.Details.NumberFormatted}}

+

{{.Details.Indicator}}

{{.Details.Context}}

{{end}}