From 37f2f547f807cee1c9c904047a0b2048290484a7 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Wed, 11 Dec 2024 21:49:41 -0500 Subject: [PATCH] sdk/check: helper methods for GitHub CheckRun API Signed-off-by: Jason Hall --- modules/github-bots/sdk/check/check.go | 93 +++++++++++++++++++++ modules/github-bots/sdk/check/check_test.go | 67 +++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 modules/github-bots/sdk/check/check.go create mode 100644 modules/github-bots/sdk/check/check_test.go diff --git a/modules/github-bots/sdk/check/check.go b/modules/github-bots/sdk/check/check.go new file mode 100644 index 00000000..3b8390a0 --- /dev/null +++ b/modules/github-bots/sdk/check/check.go @@ -0,0 +1,93 @@ +package check + +import ( + "fmt" + "strings" + + "github.com/google/go-github/v61/github" +) + +// Docs for Check Run API: https://docs.github.com/en/rest/checks/runs?apiVersion=2022-11-28 + +const ( + maxCheckOutputLength = 65536 + truncationMessage = "\n\n⚠️ _Summary has been truncated_" +) + +type Conclusion string + +const ( + ConclusionActionRequired Conclusion = "action_required" + ConclusionCancelled Conclusion = "cancelled" + ConclusionFailure Conclusion = "failure" + // ConclusionNeutral is the default, and is sufficient to pass a required check. + ConclusionNeutral Conclusion = "neutral" + ConclusionSuccess Conclusion = "success" + ConclusionTimedOut Conclusion = "timed_out" + // ConclusionSkipped is not sufficient to pass a required check. + ConclusionSkipped Conclusion = "skipped" +) + +type Builder struct { + md strings.Builder + name, headSHA string + Summary string + Conclusion Conclusion +} + +func NewBuilder(name, headSHA string) *Builder { + return &Builder{ + name: name, + headSHA: headSHA, + } +} + +// Writef appends a formatted string to the CheckRun output. +// +// If the output exceeds the maximum length, it will be truncated and a message will be appended. +func (b *Builder) Writef(format string, args ...any) { + if b.md.Len() <= maxCheckOutputLength { + b.md.WriteString(fmt.Sprintf(format, args...)) + b.md.WriteRune('\n') + } + + if b.md.Len() > maxCheckOutputLength { + out := b.md.String() + out = out[:maxCheckOutputLength-len(truncationMessage)] + out += truncationMessage + b.md = strings.Builder{} + b.md.WriteString(out) + } +} + +// CheckRun returns a GitHub CheckRun object with the current state of the Builder. +// +// If the Summary field is empty, it will be set to the name field. +// If the Conclusion field is set, the CheckRun will be marked as completed. +func (b *Builder) CheckRun() *github.CheckRun { + if b.Summary == "" { + b.Summary = b.name + } + cr := &github.CheckRun{ + Name: &b.name, + HeadSHA: &b.headSHA, + Output: &github.CheckRunOutput{ + Title: &b.Summary, + Summary: &b.Summary, + Text: github.String(b.md.String()), + }, + // Fields we don't set: + // - DetailsURL: sets the URL of the "Details" link at the bottom of the Check Run page. Defaults to the app's installation URL. + // - ExternalID: sets a unique identifier of the check run on the external system. Not used by this SDK. + // - Actions: sets actions that a user can perform on the check run. Not used by this SDK. + // - StartedAt: sets the time that the check run began. Automatically set by GitHub the first time the check run is created if it's in-progress. + // - CompletedAt: sets the time that the check run completed. Automatically set by GitHub the first time the check run is completed. + // - Output.Annotations: sets annotations that are used to provide more information about a line of code. Not used by this SDK. + } + // Providing conclusion will automatically set the status parameter to completed. + if b.Conclusion != "" { + cr.Conclusion = github.String(string(b.Conclusion)) + cr.Status = github.String("completed") + } + return cr +} diff --git a/modules/github-bots/sdk/check/check_test.go b/modules/github-bots/sdk/check/check_test.go new file mode 100644 index 00000000..dbf456ff --- /dev/null +++ b/modules/github-bots/sdk/check/check_test.go @@ -0,0 +1,67 @@ +package check + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-github/v61/github" +) + +func TestCheckRun(t *testing.T) { + b := NewBuilder("name", "headSHA") + b.Writef("test %d", 123) + + if diff := cmp.Diff(b.CheckRun(), &github.CheckRun{ + Name: github.String("name"), + HeadSHA: github.String("headSHA"), + Output: &github.CheckRunOutput{ + Title: github.String("name"), + Summary: github.String("name"), + Text: github.String("test 123\n"), + }, + }); diff != "" { + t.Errorf("CheckRun() mismatch (-want +got):\n%s", diff) + } + + b.Summary = "summary" + b.Conclusion = ConclusionSuccess + b.Writef("test %t", true) + if diff := cmp.Diff(b.CheckRun(), &github.CheckRun{ + Name: github.String("name"), + HeadSHA: github.String("headSHA"), + Status: github.String("completed"), + Conclusion: github.String("success"), + Output: &github.CheckRunOutput{ + Title: github.String("summary"), + Summary: github.String("summary"), + Text: github.String("test 123\ntest true\n"), + }, + }); diff != "" { + t.Errorf("CheckRun() mismatch (-want +got):\n%s", diff) + } +} + +func TestWritef(t *testing.T) { + b := NewBuilder("name", "headSHA") + + // append 1 KB 100 times + for i := 0; i < 100; i++ { + b.Writef(strings.Repeat("a", 1024)) //nolint:govet + + // The output should never exceed maxCheckOutputLength, even internally. + if b.md.Len() > maxCheckOutputLength { + t.Fatalf("CheckRun().Output.Text length = %d, want <= %d", b.md.Len(), maxCheckOutputLength) + } + } + + gotText := b.CheckRun().GetOutput().GetText() + wantLength := maxCheckOutputLength + if len(gotText) != wantLength { + t.Fatalf("CheckRun().Output.Text length = %d, want %d", len(gotText), wantLength) + } + if !strings.HasSuffix(gotText, truncationMessage) { + last100 := gotText[len(gotText)-100:] + t.Errorf("CheckRun().Output.Text does not have truncation message, ends with %q", last100) + } +}