diff options
author | Franck Cuny <franck@fcuny.net> | 2024-03-20 19:00:15 -0700 |
---|---|---|
committer | Franck Cuny <franck@fcuny.net> | 2024-03-20 19:03:03 -0700 |
commit | 496ee376a75798f2a1af247e6934138cad0a84e2 (patch) | |
tree | 94528bf55ae2552ee47bd61cf35239c07b092d56 | |
parent | chore: update flake (diff) | |
download | world-496ee376a75798f2a1af247e6934138cad0a84e2.tar.gz |
`ghalogs` get the logs of a GHA workflow run
Add a few internal packages to get the root of the git repository and to create clickable links in the terminal.
Diffstat (limited to '')
-rw-r--r-- | cmd/ghalogs/main.go | 139 | ||||
-rw-r--r-- | internal/git/main.go | 22 | ||||
-rw-r--r-- | internal/terminal/link.go | 13 |
3 files changed, 174 insertions, 0 deletions
diff --git a/cmd/ghalogs/main.go b/cmd/ghalogs/main.go new file mode 100644 index 0000000..f4935f0 --- /dev/null +++ b/cmd/ghalogs/main.go @@ -0,0 +1,139 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "net/http" + "os" + "strconv" + "text/tabwriter" + "time" + + "github.com/fcuny/world/internal/git" + "github.com/fcuny/world/internal/terminal" + "github.com/fcuny/world/internal/version" +) + +const API_URL = "https://api.github.com" + +const usage = `Usage: + gha-log +` + +type githubActionRun struct { + Workflows []Workflow `json:"workflow_runs"` +} + +type Workflow struct { + ID int `json:"id"` + Name string `json:"name"` + Title string `json:"display_title"` + Conclusion string `json:"conclusion"` + RunStartedAt time.Time `json:"run_started_at"` +} + +type Unmarshaler interface { + UnmarshalJSON([]byte) error +} + +func (w *Workflow) UnmarshalJSON(data []byte) error { + type Alias Workflow + aux := &struct { + RunStartedAt string `json:"run_started_at"` + *Alias + }{ + Alias: (*Alias)(w), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + runStartedAt, err := time.Parse(time.RFC3339, aux.RunStartedAt) + if err != nil { + return err + } + w.RunStartedAt = runStartedAt + return nil +} + +func main() { + flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) } + + var ( + tokenFlag string + userFlag string + versionFlag bool + ) + flag.StringVar(&tokenFlag, "token", "", "GitHub API token") + flag.StringVar(&tokenFlag, "t", "", "GitHub API token") + flag.StringVar(&userFlag, "user", "fcuny", "GitHub API token") + flag.StringVar(&userFlag, "u", "fcuny", "GitHub API token") + flag.BoolVar(&versionFlag, "version", false, "Print version information") + flag.BoolVar(&versionFlag, "v", false, "Print version information") + + if versionFlag { + information := version.VersionAndBuildInfo() + fmt.Println(information) + return + } + + flag.Parse() + + if tokenFlag == "" { + fmt.Fprintf(os.Stderr, "The API token is not set\n") + os.Exit(1) + } + + ctx := context.TODO() + + repositoryName, err := git.Root() + if err != nil { + fmt.Fprintf(os.Stderr, "could not get the repository name: %v\n", err) + os.Exit(1) + } + + url := fmt.Sprintf("%s/repos/%s/%s/actions/runs", API_URL, userFlag, repositoryName) + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + fmt.Fprintf(os.Stderr, "could not create a request: %v\n", err) + os.Exit(1) + } + + req.Header.Set("Authorization", fmt.Sprintf("token %s", tokenFlag)) + req.Header.Set("Accept", "application/vnd.github.v3+json") + req.Header.Set("X-GitHub-Api-Version", "2022-11-28") + + client := http.Client{ + Timeout: 30 * time.Second, + } + + res, err := client.Do(req) + if err != nil { + fmt.Fprintf(os.Stderr, "error making http request: %s\n", err) + os.Exit(1) + } + + if res.StatusCode != http.StatusOK { + fmt.Fprintf(os.Stderr, "unexpected status code: %d\n", res.StatusCode) + os.Exit(1) + } + + var b githubActionRun + if err := json.NewDecoder(res.Body).Decode(&b); err != nil { + fmt.Fprintf(os.Stderr, "error parsing the JSON response: %v\n", err) + os.Exit(1) + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.Debug) + for _, run := range b.Workflows { + status := "✅" + if run.Conclusion != "success" { + status = "❌" + } + + linkToAction := terminal.Link(strconv.Itoa(run.ID), fmt.Sprintf("http://github.com/%s/%s/actions/runs/%d/", userFlag, repositoryName, run.ID)) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", linkToAction, run.Name, run.RunStartedAt.Format("2006-01-02 15:04:05"), run.Title, status) + } + w.Flush() +} diff --git a/internal/git/main.go b/internal/git/main.go new file mode 100644 index 0000000..67e7d4d --- /dev/null +++ b/internal/git/main.go @@ -0,0 +1,22 @@ +package git + +import ( + "fmt" + "os/exec" + "strings" +) + +func Root() (string, error) { + cmd := exec.Command("git", "rev-parse", "--show-toplevel") + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to get git repository: %s", err) + } + + // The output includes the full path to the repository. To get just the name, + // we can split the path by "/" and take the last part. + pathParts := strings.Split(strings.TrimSpace(string(output)), "/") + repoName := pathParts[len(pathParts)-1] + + return repoName, nil +} diff --git a/internal/terminal/link.go b/internal/terminal/link.go new file mode 100644 index 0000000..a50b199 --- /dev/null +++ b/internal/terminal/link.go @@ -0,0 +1,13 @@ +package terminal + +import "fmt" + +// Link returns a formatted string that represents a hyperlink. +// The hyperlink is created using the escape sequence for terminal emulators. +// The text parameter represents the visible text of the hyperlink, +// and the url parameter represents the URL that the hyperlink points to. +// For more information on the escape sequence, refer to: +// https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#the-escape-sequence +func Link(text string, url string) string { + return fmt.Sprintf("\x1b]8;;%s\x07%s\x1b]8;;\x07\u001b[0m", url, text) +} |