diff --git a/actions.go b/actions.go index 6287c3d..f6711b5 100644 --- a/actions.go +++ b/actions.go @@ -22,6 +22,7 @@ import ( "context" "encoding/json" "fmt" + "html/template" "io" "net/http" "net/url" @@ -52,6 +53,8 @@ const ( groupCmd = "group" endGroupCmd = "endgroup" + stepSummaryCmd = "step-summary" + debugCmd = "debug" noticeCmd = "notice" warningCmd = "warning" @@ -230,6 +233,38 @@ func (c *Action) EndGroup() { }) } +// AddStepSummary writes the given markdown to the job summary. If a job summary +// already exists, this value is appended. +// +// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary +// https://github.blog/2022-05-09-supercharging-github-actions-with-job-summaries/ +func (c *Action) AddStepSummary(markdown string) { + c.IssueFileCommand(&Command{ + Name: stepSummaryCmd, + Message: markdown, + }) +} + +// AddStepSummaryTemplate adds a summary template by parsing the given Go +// template using html/template with the given input data. See AddStepSummary +// for caveats. +// +// This primarily exists as a convenience function that renders a template. +func (c *Action) AddStepSummaryTemplate(tmpl string, data any) error { + t, err := template.New("").Parse(tmpl) + if err != nil { + return fmt.Errorf("failed to parse template: %w", err) + } + + var b bytes.Buffer + if err := t.Execute(&b, data); err != nil { + return fmt.Errorf("failed to execute template: %w", err) + } + + c.AddStepSummary(b.String()) + return nil +} + // SetEnv sets an environment variable. It panics if it cannot write to the // output file. // @@ -258,7 +293,7 @@ func (c *Action) SetOutput(k, v string) { // Debugf prints a debug-level message. It follows the standard fmt.Printf // arguments, appending an OS-specific line break to the end of the message. It // panics if it cannot write to the output stream. -func (c *Action) Debugf(msg string, args ...interface{}) { +func (c *Action) Debugf(msg string, args ...any) { // ::debug :: c.IssueCommand(&Command{ Name: debugCmd, @@ -270,7 +305,7 @@ func (c *Action) Debugf(msg string, args ...interface{}) { // Noticef prints a notice-level message. It follows the standard fmt.Printf // arguments, appending an OS-specific line break to the end of the message. It // panics if it cannot write to the output stream. -func (c *Action) Noticef(msg string, args ...interface{}) { +func (c *Action) Noticef(msg string, args ...any) { // ::notice :: c.IssueCommand(&Command{ Name: noticeCmd, @@ -282,7 +317,7 @@ func (c *Action) Noticef(msg string, args ...interface{}) { // Warningf prints a warning-level message. It follows the standard fmt.Printf // arguments, appending an OS-specific line break to the end of the message. It // panics if it cannot write to the output stream. -func (c *Action) Warningf(msg string, args ...interface{}) { +func (c *Action) Warningf(msg string, args ...any) { // ::warning :: c.IssueCommand(&Command{ Name: warningCmd, @@ -294,7 +329,7 @@ func (c *Action) Warningf(msg string, args ...interface{}) { // Errorf prints a error-level message. It follows the standard fmt.Printf // arguments, appending an OS-specific line break to the end of the message. It // panics if it cannot write to the output stream. -func (c *Action) Errorf(msg string, args ...interface{}) { +func (c *Action) Errorf(msg string, args ...any) { // ::error :: c.IssueCommand(&Command{ Name: errorCmd, @@ -305,7 +340,7 @@ func (c *Action) Errorf(msg string, args ...interface{}) { // Fatalf prints a error-level message and exits. This is equivalent to Errorf // followed by os.Exit(1). -func (c *Action) Fatalf(msg string, args ...interface{}) { +func (c *Action) Fatalf(msg string, args ...any) { c.Errorf(msg, args...) osExit(1) } @@ -313,7 +348,7 @@ func (c *Action) Fatalf(msg string, args ...interface{}) { // Infof prints message to stdout without any level annotations. It follows the // standard fmt.Printf arguments, appending an OS-specific line break to the end // of the message. It panics if it cannot write to the output stream. -func (c *Action) Infof(msg string, args ...interface{}) { +func (c *Action) Infof(msg string, args ...any) { if _, err := fmt.Fprintf(c.w, msg+EOF, args...); err != nil { panic(fmt.Errorf("failed to write info command: %w", err)) } diff --git a/actions_doc_test.go b/actions_doc_test.go index 0d0559d..1eb52d7 100644 --- a/actions_doc_test.go +++ b/actions_doc_test.go @@ -38,6 +38,49 @@ func ExampleAction_AddPath() { a.AddPath("/tmp/myapp") } +func ExampleAction_GetInput() { + a := githubactions.New() + a.GetInput("foo") +} + +func ExampleAction_Group() { + a := githubactions.New() + a.Group("My group") +} + +func ExampleAction_EndGroup() { + a := githubactions.New() + a.Group("My group") + + // work + + a.EndGroup() +} + +func ExampleAction_AddStepSummary() { + a := githubactions.New() + a.AddStepSummary(` +## Heading + +- :rocket: +- :moon: +`) +} + +func ExampleAction_AddStepSummaryTemplate() { + a := githubactions.New() + if err := a.AddStepSummaryTemplate(` +## Heading + +- {{.Input}} +- :moon: +`, map[string]string{ + "Input": ":rocket:", + }); err != nil { + // handle error + } +} + func ExampleAction_Debugf() { a := githubactions.New() a.Debugf("a debug message") diff --git a/actions_root.go b/actions_root.go index f1e29bd..a252e1e 100644 --- a/actions_root.go +++ b/actions_root.go @@ -14,7 +14,9 @@ package githubactions -import "context" +import ( + "context" +) var ( defaultAction = New() @@ -71,6 +73,21 @@ func EndGroup() { defaultAction.EndGroup() } +// AddStepSummary writes the given markdown to the job summary. If a job summary +// already exists, this value is appended. +func AddStepSummary(markdown string) { + defaultAction.AddStepSummary(markdown) +} + +// AddStepSummaryTemplate adds a summary template by parsing the given Go +// template using html/template with the given input data. See AddStepSummary +// for caveats. +// +// This primarily exists as a convenience function that renders a template. +func AddStepSummaryTemplate(tmpl string, data any) error { + return defaultAction.AddStepSummaryTemplate(tmpl, data) +} + // SetEnv sets an environment variable. func SetEnv(k, v string) { defaultAction.SetEnv(k, v) @@ -83,31 +100,31 @@ func SetOutput(k, v string) { // Debugf prints a debug-level message. The arguments follow the standard Printf // arguments. -func Debugf(msg string, args ...interface{}) { +func Debugf(msg string, args ...any) { defaultAction.Debugf(msg, args...) } // Errorf prints a error-level message. The arguments follow the standard Printf // arguments. -func Errorf(msg string, args ...interface{}) { +func Errorf(msg string, args ...any) { defaultAction.Errorf(msg, args...) } // Fatalf prints a error-level message and exits. This is equivalent to Errorf // followed by os.Exit(1). -func Fatalf(msg string, args ...interface{}) { +func Fatalf(msg string, args ...any) { defaultAction.Fatalf(msg, args...) } // Infof prints a info-level message. The arguments follow the standard Printf // arguments. -func Infof(msg string, args ...interface{}) { +func Infof(msg string, args ...any) { defaultAction.Infof(msg, args...) } // Warningf prints a warning-level message. The arguments follow the standard // Printf arguments. -func Warningf(msg string, args ...interface{}) { +func Warningf(msg string, args ...any) { defaultAction.Warningf(msg, args...) } diff --git a/actions_test.go b/actions_test.go index acab714..cb0ca24 100644 --- a/actions_test.go +++ b/actions_test.go @@ -144,8 +144,6 @@ func TestAction_RemoveMatcher(t *testing.T) { func TestAction_AddPath(t *testing.T) { t.Parallel() - const envGitHubPath = "GITHUB_PATH" - // expect a file command to be issued when env file is set. file, err := os.CreateTemp("", "") if err != nil { @@ -153,7 +151,7 @@ func TestAction_AddPath(t *testing.T) { } defer os.Remove(file.Name()) - fakeGetenvFunc := newFakeGetenvFunc(t, envGitHubPath, file.Name()) + fakeGetenvFunc := newFakeGetenvFunc(t, "GITHUB_PATH", file.Name()) var b bytes.Buffer a := New(WithWriter(&b), WithGetenv(fakeGetenvFunc)) @@ -227,10 +225,86 @@ func TestAction_EndGroup(t *testing.T) { } } -func TestAction_SetEnv(t *testing.T) { +func TestAction_AddStepSummary(t *testing.T) { + t.Parallel() + + // expectations for env file env commands + var b bytes.Buffer + file, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("unable to create a temp env file: %s", err) + } + + defer os.Remove(file.Name()) + fakeGetenvFunc := newFakeGetenvFunc(t, "GITHUB_STEP_SUMMARY", file.Name()) + a := New(WithWriter(&b), WithGetenv(fakeGetenvFunc)) + a.AddStepSummary(` +## This is + +some markdown +`) + a.AddStepSummary(` +- content +`) + + // expect an empty stdout buffer + if got, want := b.String(), ""; got != want { + t.Errorf("expected %q to be %q", got, want) + } + + // expect the command to be written to the file. + data, err := io.ReadAll(file) + if err != nil { + t.Errorf("unable to read temp summary file: %s", err) + } + + want := "\n## This is\n\nsome markdown\n" + EOF + "\n- content\n" + EOF + if got := string(data); got != want { + t.Errorf("expected %q to be %q", got, want) + } +} + +func TestAction_AddStepSummaryTemplate(t *testing.T) { t.Parallel() - const envGitHubEnv = "GITHUB_ENV" + // expectations for env file env commands + var b bytes.Buffer + file, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("unable to create a temp env file: %s", err) + } + + defer os.Remove(file.Name()) + fakeGetenvFunc := newFakeGetenvFunc(t, "GITHUB_STEP_SUMMARY", file.Name()) + a := New(WithWriter(&b), WithGetenv(fakeGetenvFunc)) + a.AddStepSummaryTemplate(` +## This is + +{{.Input}} +- content +`, map[string]string{ + "Input": "some markdown", + }) + + // expect an empty stdout buffer + if got, want := b.String(), ""; got != want { + t.Errorf("expected %q to be %q", got, want) + } + + // expect the command to be written to the file. + data, err := io.ReadAll(file) + if err != nil { + t.Errorf("unable to read temp summary file: %s", err) + } + + want := "\n## This is\n\nsome markdown\n- content\n" + EOF + if got := string(data); got != want { + t.Errorf("expected %q to be %q", got, want) + } +} + +func TestAction_SetEnv(t *testing.T) { + t.Parallel() // expectations for env file env commands var b bytes.Buffer @@ -240,7 +314,7 @@ func TestAction_SetEnv(t *testing.T) { } defer os.Remove(file.Name()) - fakeGetenvFunc := newFakeGetenvFunc(t, envGitHubEnv, file.Name()) + fakeGetenvFunc := newFakeGetenvFunc(t, "GITHUB_ENV", file.Name()) a := New(WithWriter(&b), WithGetenv(fakeGetenvFunc)) a.SetEnv("key", "value") a.SetEnv("key2", "value2")