Skip to content

Commit

Permalink
Add Action.getenv and use variadic options in New(). (#19)
Browse files Browse the repository at this point in the history
* Adding `Action` options.

* Using options in `New()`.

* Adding `Action.getenv` and `OptGetEnv` helper.

* Adding godoc for `Opt*()` helpers.

* Adding `getenv` default in `New()`.

* Replacing `os.Getenv` usage with `Action.getenv`.

* Ditching `Action.setEnv` in favor of using `OptGetenv`.

* Ditching `Action.addPath` in favor of using `OptGetenv`.

* Ditching `Action.getInput` in favor of using `OptGetenv`.

* Ditching `Action.issueFileCommand` in favor of using `OptGetenv`.

* Ditching `NewWithWriter` in tests.

This will make it easier to deprecate `NewWithWriter` later.

* Adding test for `NewWithWriter`.

* Fix year in new `options_test.go`.

* Modifying `Option` so it also returns an `Action`.

* Adding a `nil`-guard for `opt` in `New()`.

* Adding deprecation notice to godoc for `NewWithWriter()`.

* Renaming `Opt*` to `With*` for options.

The diff was completely generated via:

```
git grep -l OptWriter | xargs sed -i '' s/OptWriter/WithWriter/g
git grep -l OptFields | xargs sed -i '' s/OptFields/WithFields/g
git grep -l OptGetenv | xargs sed -i '' s/OptGetenv/WithGetenv/g
```

so reviewing the whole diff is not necessary (if that makes code
review easier).

* Exporting `getenvFunc`.

Done via:

```
git grep -l getenvFunc | xargs sed -i '' s/getenvFunc/GetenvFunc/g
```
  • Loading branch information
dhermes committed Apr 21, 2021
1 parent 3282af2 commit fb5dddc
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 56 deletions.
48 changes: 21 additions & 27 deletions actions.go
Expand Up @@ -53,22 +53,32 @@ const (

// New creates a new wrapper with helpers for outputting information in GitHub
// actions format.
func New() *Action {
return &Action{w: os.Stdout}
func New(opts ...Option) *Action {
a := &Action{w: os.Stdout, getenv: os.Getenv}
for _, opt := range opts {
if opt == nil {
continue
}
a = opt(a)
}
return a
}

// NewWithWriter creates a wrapper using the given writer. This is useful for
// tests. The given writer cannot add any prefixes to the string, since GitHub
// requires these special strings to match a very particular format.
//
// Deprecated: Use New() with WithWriter instead.
func NewWithWriter(w io.Writer) *Action {
return &Action{w: w}
return New(WithWriter(w))
}

// Action is an internal wrapper around GitHub Actions' output and magic
// strings.
type Action struct {
w io.Writer
fields CommandProperties
getenv GetenvFunc
}

// IssueCommand issues a new GitHub actions Command.
Expand All @@ -88,15 +98,11 @@ func (c *Action) IssueCommand(cmd *Command) {
// with the 'Command' argument as it's scope is unclear in the current
// TypeScript implementation.
func (c *Action) IssueFileCommand(cmd *Command) error {
return c.issueFileCommand(cmd, os.Getenv)
}

func (c *Action) issueFileCommand(cmd *Command, f getenvFunc) error {
e := strings.ReplaceAll(cmd.Name, "-", "_")
e = strings.ToUpper(e)
e = "GITHUB_" + e

err := ioutil.WriteFile(f(e), []byte(cmd.Message+"\n"), os.ModeAppend)
err := ioutil.WriteFile(c.getenv(e), []byte(cmd.Message+"\n"), os.ModeAppend)
if err != nil {
return fmt.Errorf(errFileCmdFmt, err)
}
Expand Down Expand Up @@ -143,14 +149,10 @@ func (c *Action) RemoveMatcher(o string) {
// https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#adding-a-system-path
// https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/
func (c *Action) AddPath(p string) {
c.addPath(p, os.Getenv)
}

func (c *Action) addPath(p string, f getenvFunc) {
err := c.issueFileCommand(&Command{
err := c.IssueFileCommand(&Command{
Name: pathCmd,
Message: p,
}, f)
})

if err != nil { // use regular command as fallback
// ::add-path::<p>
Expand All @@ -175,14 +177,10 @@ func (c *Action) SaveState(k, v string) {

// GetInput gets the input by the given name.
func (c *Action) GetInput(i string) string {
return c.getInput(i, os.Getenv)
}

func (c *Action) getInput(i string, f getenvFunc) string {
e := strings.ReplaceAll(i, " ", "_")
e = strings.ToUpper(e)
e = "INPUT_" + e
return strings.TrimSpace(f(e))
return strings.TrimSpace(c.getenv(e))
}

// Group starts a new collapsable region up to the next ungroup invocation.
Expand Down Expand Up @@ -210,14 +208,10 @@ func (c *Action) EndGroup() {
// https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable
// https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/
func (c *Action) SetEnv(k, v string) {
c.setEnv(k, v, os.Getenv)
}

func (c *Action) setEnv(k, v string, f getenvFunc) {
err := c.issueFileCommand(&Command{
err := c.IssueFileCommand(&Command{
Name: envCmd,
Message: fmt.Sprintf(envCmdMsgFmt, k, envCmdDelimiter, v, envCmdDelimiter),
}, f)
})

if err != nil { // use regular command as fallback
// ::set-env name=<k>::<v>
Expand Down Expand Up @@ -316,6 +310,6 @@ func (c *Action) WithFieldsMap(m map[string]string) *Action {
}
}

// getenvFunc is an abstraction to make tests feasible for commands that
// GetenvFunc is an abstraction to make tests feasible for commands that
// interact with environment variables.
type getenvFunc func(k string) string
type GetenvFunc func(k string) string
74 changes: 45 additions & 29 deletions actions_test.go
Expand Up @@ -21,11 +21,27 @@ import (
"testing"
)

func TestAction_IssueCommand(t *testing.T) {
func TestNewWithWriter(t *testing.T) {
t.Parallel()

var b bytes.Buffer
a := NewWithWriter(&b)

a.IssueCommand(&Command{
Name: "foo",
Message: "bar",
})

if got, want := b.String(), "::foo::bar\n"; got != want {
t.Errorf("expected %q to be %q", got, want)
}
}

func TestAction_IssueCommand(t *testing.T) {
t.Parallel()

var b bytes.Buffer
a := New(WithWriter(&b))
a.IssueCommand(&Command{
Name: "foo",
Message: "bar",
Expand All @@ -48,12 +64,12 @@ func TestAction_IssueFileCommand(t *testing.T) {

fakeGetenvFunc := newFakeGetenvFunc(t, "GITHUB_FOO", file.Name())
var b bytes.Buffer
a := NewWithWriter(&b)
a := New(WithWriter(&b), WithGetenv(fakeGetenvFunc))

err = a.issueFileCommand(&Command{
err = a.IssueFileCommand(&Command{
Name: "foo",
Message: "bar",
}, fakeGetenvFunc)
})

if err != nil {
t.Errorf("expected nil error, got: %s", err)
Expand All @@ -79,7 +95,7 @@ func TestAction_AddMask(t *testing.T) {
t.Parallel()

var b bytes.Buffer
a := NewWithWriter(&b)
a := New(WithWriter(&b))
a.AddMask("foobar")

if got, want := b.String(), "::add-mask::foobar\n"; got != want {
Expand All @@ -91,7 +107,7 @@ func TestAction_AddMatcher(t *testing.T) {
t.Parallel()

var b bytes.Buffer
a := NewWithWriter(&b)
a := New(WithWriter(&b))
a.AddMatcher("foobar.json")

if got, want := b.String(), "::add-matcher::foobar.json\n"; got != want {
Expand All @@ -103,7 +119,7 @@ func TestAction_RemoveMatcher(t *testing.T) {
t.Parallel()

var b bytes.Buffer
a := NewWithWriter(&b)
a := New(WithWriter(&b))
a.RemoveMatcher("foobar")

if got, want := b.String(), "::remove-matcher owner=foobar::\n"; got != want {
Expand All @@ -119,9 +135,9 @@ func TestAction_AddPath(t *testing.T) {
// expect a regular command to be issued when env file is not set.
fakeGetenvFunc := newFakeGetenvFunc(t, envGitHubPath, "")
var b bytes.Buffer
a := NewWithWriter(&b)
a := New(WithWriter(&b), WithGetenv(fakeGetenvFunc))

a.addPath("/custom/bin", fakeGetenvFunc)
a.AddPath("/custom/bin")
if got, want := b.String(), "::add-path::/custom/bin\n"; got != want {
t.Errorf("expected %q to be %q", got, want)
}
Expand All @@ -136,8 +152,9 @@ func TestAction_AddPath(t *testing.T) {

defer os.Remove(file.Name())
fakeGetenvFunc = newFakeGetenvFunc(t, envGitHubPath, file.Name())
WithGetenv(fakeGetenvFunc)(a)

a.addPath("/custom/bin", fakeGetenvFunc)
a.AddPath("/custom/bin")

if got, want := b.String(), ""; got != want {
t.Errorf("expected %q to be %q", got, want)
Expand All @@ -163,7 +180,7 @@ func TestAction_SaveState(t *testing.T) {
t.Parallel()

var b bytes.Buffer
a := NewWithWriter(&b)
a := New(WithWriter(&b))
a.SaveState("key", "value")

if got, want := b.String(), "::save-state name=key::value\n"; got != want {
Expand All @@ -177,8 +194,8 @@ func TestAction_GetInput(t *testing.T) {
fakeGetenvFunc := newFakeGetenvFunc(t, "INPUT_FOO", "bar")

var b bytes.Buffer
a := NewWithWriter(&b)
if got, want := a.getInput("foo", fakeGetenvFunc), "bar"; got != want {
a := New(WithWriter(&b), WithGetenv(fakeGetenvFunc))
if got, want := a.GetInput("foo"), "bar"; got != want {
t.Errorf("expected %q to be %q", got, want)
}
}
Expand All @@ -187,7 +204,7 @@ func TestAction_Group(t *testing.T) {
t.Parallel()

var b bytes.Buffer
a := NewWithWriter(&b)
a := New(WithWriter(&b))
a.Group("mygroup")

if got, want := b.String(), "::group::mygroup\n"; got != want {
Expand All @@ -199,7 +216,7 @@ func TestAction_EndGroup(t *testing.T) {
t.Parallel()

var b bytes.Buffer
a := NewWithWriter(&b)
a := New(WithWriter(&b))
a.EndGroup()

if got, want := b.String(), "::endgroup::\n"; got != want {
Expand All @@ -223,25 +240,24 @@ func TestAction_SetEnv(t *testing.T) {
for _, check := range checks {
fakeGetenvFunc := newFakeGetenvFunc(t, envGitHubEnv, "")
var b bytes.Buffer
a := NewWithWriter(&b)
a.setEnv(check.key, check.value, fakeGetenvFunc)
a := New(WithWriter(&b), WithGetenv(fakeGetenvFunc))
a.SetEnv(check.key, check.value)
if got, want := b.String(), check.want; got != want {
t.Errorf("SetEnv(%q, %q): expected %q; got %q", check.key, check.value, want, got)
}
}

// expectations for env file env commands
var b bytes.Buffer
a := NewWithWriter(&b)
file, err := ioutil.TempFile(".", ".set_env_test_")
if err != nil {
t.Fatalf("unable to create a temp env file: %s", err)
}

defer os.Remove(file.Name())
fakeGetenvFunc := newFakeGetenvFunc(t, envGitHubEnv, file.Name())

a.setEnv("key", "value", fakeGetenvFunc)
a := New(WithWriter(&b), WithGetenv(fakeGetenvFunc))
a.SetEnv("key", "value")

// expect an empty stdout buffer
if got, want := b.String(), ""; got != want {
Expand All @@ -264,7 +280,7 @@ func TestAction_SetOutput(t *testing.T) {
t.Parallel()

var b bytes.Buffer
a := NewWithWriter(&b)
a := New(WithWriter(&b))
a.SetOutput("key", "value")

if got, want := b.String(), "::set-output name=key::value\n"; got != want {
Expand All @@ -276,7 +292,7 @@ func TestAction_Debugf(t *testing.T) {
t.Parallel()

var b bytes.Buffer
a := NewWithWriter(&b)
a := New(WithWriter(&b))
a.Debugf("fail: %s", "thing")

if got, want := b.String(), "::debug::fail: thing\n"; got != want {
Expand All @@ -288,7 +304,7 @@ func TestAction_Errorf(t *testing.T) {
t.Parallel()

var b bytes.Buffer
a := NewWithWriter(&b)
a := New(WithWriter(&b))
a.Errorf("fail: %s", "thing")

if got, want := b.String(), "::error::fail: thing\n"; got != want {
Expand All @@ -300,7 +316,7 @@ func TestAction_Warningf(t *testing.T) {
t.Parallel()

var b bytes.Buffer
a := NewWithWriter(&b)
a := New(WithWriter(&b))
a.Warningf("fail: %s", "thing")

if got, want := b.String(), "::warning::fail: thing\n"; got != want {
Expand All @@ -312,7 +328,7 @@ func TestAction_Infof(t *testing.T) {
t.Parallel()

var b bytes.Buffer
a := NewWithWriter(&b)
a := New(WithWriter(&b))
a.Infof("info: %s\n", "thing")

if got, want := b.String(), "info: thing\n"; got != want {
Expand All @@ -324,7 +340,7 @@ func TestAction_WithFieldsSlice(t *testing.T) {
t.Parallel()

var b bytes.Buffer
a := NewWithWriter(&b)
a := New(WithWriter(&b))
a = a.WithFieldsSlice([]string{"line=100", "file=app.js"})
a.Debugf("fail: %s", "thing")

Expand All @@ -337,7 +353,7 @@ func TestAction_WithFieldsMap(t *testing.T) {
t.Parallel()

var b bytes.Buffer
a := NewWithWriter(&b)
a := New(WithWriter(&b))
a = a.WithFieldsMap(map[string]string{"line": "100", "file": "app.js"})
a.Debugf("fail: %s", "thing")

Expand All @@ -346,10 +362,10 @@ func TestAction_WithFieldsMap(t *testing.T) {
}
}

// newFakeGetenvFunc returns a new getenvFunc that is expected to be called with
// newFakeGetenvFunc returns a new GetenvFunc that is expected to be called with
// the provided key. It returns the provided value if the call matches the
// provided key. It reports an error on test t otherwise.
func newFakeGetenvFunc(t *testing.T, wantKey, v string) getenvFunc {
func newFakeGetenvFunc(t *testing.T, wantKey, v string) GetenvFunc {
return func(gotKey string) string {
if gotKey != wantKey {
t.Errorf("expected call GetenvFunc(%q) to be GetenvFunc(%q)", gotKey, wantKey)
Expand Down

0 comments on commit fb5dddc

Please sign in to comment.