about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFranck Cuny <franck@fcuny.net>2024-03-20 19:00:15 -0700
committerFranck Cuny <franck@fcuny.net>2024-03-20 19:03:03 -0700
commit496ee376a75798f2a1af247e6934138cad0a84e2 (patch)
tree94528bf55ae2552ee47bd61cf35239c07b092d56
parentchore: update flake (diff)
downloadworld-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.go139
-rw-r--r--internal/git/main.go22
-rw-r--r--internal/terminal/link.go13
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)
+}