diff --git a/client.go b/client.go deleted file mode 100644 index 9898adb..0000000 --- a/client.go +++ /dev/null @@ -1,33 +0,0 @@ -package sdk - -import ( - "fmt" - "net/http" - "net/url" -) - -func (a *Action) Client() *Client { - c := &Client{Client: &http.Client{}} - context := a.Context() - c.base = context.APIURL - c.token = fmt.Sprintf("Bearer %s", context.Token) - return c -} - -type Client struct { - *http.Client - base string - token string -} - -func (c *Client) Do(req *http.Request) (*http.Response, error) { - req.Header.Set("Authorization", c.token) - if !req.URL.IsAbs() { - u, err := url.Parse(fmt.Sprintf("%s%s", c.base, req.URL)) - if err != nil { - return nil, err - } - req.URL = u - } - return c.Client.Do(req) -} diff --git a/context.go b/context.go index 5f676c9..76477d2 100644 --- a/context.go +++ b/context.go @@ -2,8 +2,10 @@ package sdk import ( "encoding/json" + "errors" "fmt" "os" + "strconv" ) // GitHubContext of current workflow. @@ -12,7 +14,7 @@ type GitHubContext struct { Action string `env:"GITHUB_ACTION"` ActionPath string `env:"GITHUB_ACTION_PATH"` ActionRepository string `env:"GITHUB_ACTION_REPOSITORY"` - Actions string `env:"GITHUB_ACTIONS"` + Actions bool `env:"GITHUB_ACTIONS"` Actor string `env:"GITHUB_ACTOR"` APIURL string `env:"GITHUB_API_URL,default=https://api.github.com"` BaseRef string `env:"GITHUB_BASE_REF"` @@ -25,36 +27,34 @@ type GitHubContext struct { Path string `env:"GITHUB_PATH"` Ref string `env:"GITHUB_REF"` RefName string `env:"GITHUB_REF_NAME"` - RefProtected string `env:"GITHUB_REF_PROTECTED"` + RefProtected bool `env:"GITHUB_REF_PROTECTED"` RefType string `env:"GITHUB_REF_TYPE"` Repository string `env:"GITHUB_REPOSITORY"` RepositoryOwner string `env:"GITHUB_REPOSITORY_OWNER"` - RetentionDays string `env:"GITHUB_RETENTION_DAYS"` - RunAttempt string `env:"GITHUB_RUN_ATTEMPT"` - RunID string `env:"GITHUB_RUN_ID"` - RunNumber string `env:"GITHUB_RUN_NUMBER"` + RetentionDays int64 `env:"GITHUB_RETENTION_DAYS"` + RunAttempt int64 `env:"GITHUB_RUN_ATTEMPT"` + RunID int64 `env:"GITHUB_RUN_ID"` + RunNumber int64 `env:"GITHUB_RUN_NUMBER"` ServerURL string `env:"GITHUB_SERVER_URL,default=https://github.com"` SHA string `env:"GITHUB_SHA"` StepSummary string `env:"GITHUB_STEP_SUMMARY"` Workflow string `env:"GITHUB_WORKFLOW"` Workspace string `env:"GITHUB_WORKSPACE"` - Token string `env:"GITHUB_TOKEN"` - // Event is populated by parsing the file at EventPath, if it exists. - event map[string]any + Event map[string]any } // Context returns the context of current action with the payload object // that triggered the workflow -func (c *Action) Context() *GitHubContext { +func (c *Action) Context() (*GitHubContext, error) { + var merr error context := &GitHubContext{ APIURL: "https://api.github.com", GraphqlURL: "https://api.github.com/graphql", ServerURL: "https://github.com", - event: map[string]any{}, } if v := c.env("GITHUB_ACTION"); v != "" { @@ -66,8 +66,10 @@ func (c *Action) Context() *GitHubContext { if v := c.env("GITHUB_ACTION_REPOSITORY"); v != "" { context.ActionRepository = v } - if v := c.env("GITHUB_ACTIONS"); v != "" { + if v, err := parseBool(c.env("GITHUB_ACTIONS")); err == nil { context.Actions = v + } else { + merr = errors.Join(merr, err) } if v := c.env("GITHUB_ACTOR"); v != "" { context.Actor = v @@ -105,29 +107,41 @@ func (c *Action) Context() *GitHubContext { if v := c.env("GITHUB_REF_NAME"); v != "" { context.RefName = v } - if v := c.env("GITHUB_REF_PROTECTED"); v != "" { + if v, err := parseBool(c.env("GITHUB_REF_PROTECTED")); err == nil { context.RefProtected = v + } else { + merr = errors.Join(merr, err) } if v := c.env("GITHUB_REF_TYPE"); v != "" { context.RefType = v } + if v := c.env("GITHUB_REPOSITORY"); v != "" { context.Repository = v } if v := c.env("GITHUB_REPOSITORY_OWNER"); v != "" { context.RepositoryOwner = v } - if v := c.env("GITHUB_RETENTION_DAYS"); v != "" { + + if v, err := parseInt(c.env("GITHUB_RETENTION_DAYS")); err == nil { context.RetentionDays = v + } else { + merr = errors.Join(merr, err) } - if v := c.env("GITHUB_RUN_ATTEMPT"); v != "" { + if v, err := parseInt(c.env("GITHUB_RUN_ATTEMPT")); err == nil { context.RunAttempt = v + } else { + merr = errors.Join(merr, err) } - if v := c.env("GITHUB_RUN_ID"); v != "" { + if v, err := parseInt(c.env("GITHUB_RUN_ID")); err == nil { context.RunID = v + } else { + merr = errors.Join(merr, err) } - if v := c.env("GITHUB_RUN_NUMBER"); v != "" { + if v, err := parseInt(c.env("GITHUB_RUN_NUMBER")); err == nil { context.RunNumber = v + } else { + merr = errors.Join(merr, err) } if v := c.env("GITHUB_SERVER_URL"); v != "" { context.ServerURL = v @@ -144,24 +158,32 @@ func (c *Action) Context() *GitHubContext { if v := c.env("GITHUB_WORKSPACE"); v != "" { context.Workspace = v } - if v := c.env("GITHUB_TOKEN"); v != "" { - context.Token = v - } - return context -} - -func (c *GitHubContext) Event() (map[string]any, error) { - if c.EventPath != "" { - eventData, err := os.ReadFile(c.EventPath) + if context.EventPath != "" { + eventData, err := os.ReadFile(context.EventPath) if err != nil && !os.IsNotExist(err) { return nil, fmt.Errorf("could not read event file: %w", err) } if eventData != nil { - if err := json.Unmarshal(eventData, &c.event); err != nil { + if err := json.Unmarshal(eventData, &context.Event); err != nil { return nil, fmt.Errorf("failed to unmarshal event payload: %w", err) } } } - return c.event, nil + + return context, merr +} + +func parseBool(v string) (bool, error) { + if v == "" { + return false, nil + } + return strconv.ParseBool(v) +} + +func parseInt(v string) (int64, error) { + if v == "" { + return 0, nil + } + return strconv.ParseInt(v, 10, 64) } diff --git a/context_test.go b/context_test.go index 89f3ab2..f261d42 100644 --- a/context_test.go +++ b/context_test.go @@ -37,7 +37,6 @@ func TestAction_Context(t *testing.T) { APIURL: "https://api.github.com", ServerURL: "https://github.com", GraphqlURL: "https://api.github.com/graphql", - event: map[string]any{}, }, }, { @@ -71,13 +70,12 @@ func TestAction_Context(t *testing.T) { "GITHUB_STEP_SUMMARY": "/path/to/summary", "GITHUB_WORKFLOW": "test", "GITHUB_WORKSPACE": "/path/to/workspace", - "GITHUB_TOKEN": "somerandomtoken", }, exp: &GitHubContext{ Action: "__repo-owner_name-of-action-repo", ActionPath: "/path/to/action", ActionRepository: "repo-owner/name-of-action-repo", - Actions: "true", + Actions: true, Actor: "sethvargo", APIURL: "https://foo.com", BaseRef: "main", @@ -90,21 +88,19 @@ func TestAction_Context(t *testing.T) { Path: "/path/to/path", Ref: "refs/tags/v1.0", RefName: "v1.0", - RefProtected: "true", + RefProtected: true, RefType: "tag", Repository: "sethvargo/baz", RepositoryOwner: "sethvargo", - RetentionDays: "90", - RunAttempt: "6", - RunID: "56", - RunNumber: "34", + RetentionDays: 90, + RunAttempt: 6, + RunID: 56, + RunNumber: 34, ServerURL: "https://bar.com", SHA: "abcd1234", StepSummary: "/path/to/summary", Workflow: "test", Workspace: "/path/to/workspace", - Token: "somerandomtoken", - event: map[string]any{}, }, }, { @@ -120,7 +116,7 @@ func TestAction_Context(t *testing.T) { ServerURL: "https://github.com", GraphqlURL: "https://api.github.com/graphql", - event: map[string]any{ + Event: map[string]any{ "foo": "bar", }, }, @@ -135,8 +131,7 @@ func TestAction_Context(t *testing.T) { a := New() a.env = func(s string) string { return tc.env[s] } - got := a.Context() - _, err := got.Event() + got, err := a.Context() if err != nil { t.Fatal(err) } diff --git a/go.mod b/go.mod index f788a0e..5f045b8 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module code.geekeey.de/actions/sdk +module git.geekeey.de/actions/sdk go 1.22.5 diff --git a/iox/globfs.go b/iox/globfs.go deleted file mode 100644 index 91240cb..0000000 --- a/iox/globfs.go +++ /dev/null @@ -1,99 +0,0 @@ -package iox - -import ( - "io/fs" - "os" - "path" -) - -type GlobFS struct { - base fs.FS - patterns []string -} - -// NewGlobFS creates a new GlobFS that exposes only files matching any of the given glob patterns. -func NewGlobFS(base fs.FS, patterns ...string) *GlobFS { - return &GlobFS{base: base, patterns: patterns} -} - -// match reports whether the given path matches any of the configured patterns. -func (g *GlobFS) match(name string) bool { - for _, pat := range g.patterns { - if matched, _ := path.Match(pat, name); matched { - return true - } - } - return false -} - -func (g *GlobFS) contains(dir string) bool { - queue := []string{dir} - visited := make(map[string]struct{}) - for len(queue) > 0 { - current := queue[0] - queue = queue[1:] // dequeue - - // Prevent visiting same dir multiple times - if _, seen := visited[current]; seen { - continue - } - visited[current] = struct{}{} - - entries, err := fs.ReadDir(g.base, current) - if err != nil { - continue - } - for _, entry := range entries { - rel := path.Join(current, entry.Name()) - if g.match(rel) { - return true - } - if entry.IsDir() { - queue = append(queue, rel) - } - } - } - return false -} - -func (g *GlobFS) Open(name string) (fs.File, error) { - if g.match(name) { - return g.base.Open(name) - } - - fi, err := fs.Stat(g.base, name) - if err != nil || !fi.IsDir() { - return nil, fs.ErrNotExist - } - if g.contains(name) { - return g.base.Open(name) - } - return nil, fs.ErrNotExist -} - -func (g *GlobFS) ReadDir(name string) ([]fs.DirEntry, error) { - if g.match(name) { - return fs.ReadDir(g.base, name) - } - - entries, err := fs.ReadDir(g.base, name) - if err != nil { - return nil, err - } - var children []fs.DirEntry - for _, entry := range entries { - rel := path.Join(name, entry.Name()) - if g.match(rel) { - children = append(children, entry) - } - if entry.IsDir() && g.contains(rel) { - children = append(children, entry) - } - } - - if len(children) == 0 { - return nil, os.ErrNotExist - } - - return children, nil -} diff --git a/iox/globfs_test.go b/iox/globfs_test.go deleted file mode 100644 index e548a2a..0000000 --- a/iox/globfs_test.go +++ /dev/null @@ -1,234 +0,0 @@ -package iox - -import ( - "io/fs" - "reflect" - "sort" - "testing" - "testing/fstest" -) - -func setupFS() fs.ReadDirFS { - // Create an in-memory FS with a mix of files and directories - return fstest.MapFS{ - "main.go": &fstest.MapFile{Data: []byte("package main")}, - "main_test.go": &fstest.MapFile{Data: []byte("package main_test")}, - "README.md": &fstest.MapFile{Data: []byte("# readme")}, - "LICENSE": &fstest.MapFile{Data: []byte("MIT")}, - "docs/guide.md": &fstest.MapFile{Data: []byte("Docs")}, - "docs/other.txt": &fstest.MapFile{Data: []byte("Other")}, - "docs/hidden/.keep": &fstest.MapFile{Data: []byte("")}, - "assets/img.png": &fstest.MapFile{Data: []byte("PNG")}, - "assets/style.css": &fstest.MapFile{Data: []byte("CSS")}, - ".gitignore": &fstest.MapFile{Data: []byte("*.log")}, - ".hiddenfile": &fstest.MapFile{Data: []byte("")}, - "emptydir/": &fstest.MapFile{Mode: fs.ModeDir}, - } -} - -// helper to get base names for easier comparison -func basenames(entries []fs.DirEntry) []string { - names := []string{} - for _, e := range entries { - names = append(names, e.Name()) - } - sort.Strings(names) - return names -} - -func TestGlobFSMultiplePatterns(t *testing.T) { - memfs := setupFS() - gfs := NewGlobFS(memfs, "*.go", "*.md", "assets/*", "docs/guide.md", ".gitignore") - - tests := []struct { - path string - want []string - wantErr bool - }{ - {path: ".", want: []string{"README.md", "assets", "docs", "main.go", "main_test.go", ".gitignore"}}, - {path: "assets", want: []string{"img.png", "style.css"}}, - {path: "docs", want: []string{"guide.md"}}, - {path: "docs/hidden", want: []string{}, wantErr: true}, - {path: "emptydir", want: []string{}, wantErr: true}, - } - - for _, tc := range tests { - tc := tc // capture range variable - t.Run(tc.path, func(t *testing.T) { - entries, err := fs.ReadDir(gfs, tc.path) - if tc.wantErr && err == nil { - t.Errorf("expected error, got nil") - return - } - if !tc.wantErr && err != nil { - t.Errorf("unexpected error: %v", err) - return - } - got := basenames(entries) - sort.Strings(tc.want) - if !reflect.DeepEqual(got, tc.want) { - t.Errorf("got %v; want %v", got, tc.want) - } - }) - } -} - -func TestGlobFSOpen(t *testing.T) { - memfs := setupFS() - gfs := NewGlobFS(memfs, "*.go", "*.md", "assets/*", "docs/guide.md", ".gitignore") - - type test struct { - path string - wantErr bool - } - tests := []test{ - {path: "main.go"}, - {path: "README.md"}, - {path: "LICENSE", wantErr: true}, - {path: "assets/img.png"}, - {path: "assets/style.css"}, - {path: "assets/nonexistent.png", wantErr: true}, - {path: "docs/guide.md"}, - {path: "docs/other.txt", wantErr: true}, - {path: ".gitignore"}, - {path: ".hiddenfile", wantErr: true}, - {path: "docs/hidden/.keep", wantErr: true}, - {path: "emptydir", wantErr: true}, - {path: "docs"}, // allowed because it contains matching file(s) - {path: "assets"}, // allowed because it contains matching file(s) - } - for _, tc := range tests { - tc := tc - t.Run(tc.path, func(t *testing.T) { - f, err := gfs.Open(tc.path) - if tc.wantErr && err == nil { - t.Errorf("expected error, got file") - if f != nil { - f.Close() - } - } else if !tc.wantErr && err != nil { - t.Errorf("unexpected error: %v", err) - } else if !tc.wantErr && err == nil { - info, _ := f.Stat() - if info.IsDir() { - _, derr := fs.ReadDir(gfs, tc.path) - if derr != nil && !tc.wantErr { - t.Errorf("unexpected error: %v", derr) - } - } - f.Close() - } - }) - } -} - -func TestGlobFSReadFile(t *testing.T) { - memfs := setupFS() - gfs := NewGlobFS(memfs, "*.go", "*.md", "assets/*", ".gitignore") - - tests := []struct { - name string - want []byte - wantErr bool - }{ - {name: "main.go", want: []byte("package main")}, - {name: "main_test.go", want: []byte("package main_test")}, - {name: "README.md", want: []byte("# readme")}, - {name: "assets/img.png", want: []byte("PNG")}, - {name: "assets/style.css", want: []byte("CSS")}, - {name: ".gitignore", want: []byte("*.log")}, - {name: "LICENSE", wantErr: true}, // not allowed by filter - {name: "docs/guide.md", wantErr: true}, // not allowed by filter - {name: "docs/hidden/.keep", wantErr: true}, // not allowed by filter - {name: "doesnotexist.txt", wantErr: true}, // does not exist - } - - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - got, err := fs.ReadFile(gfs, tc.name) - if tc.wantErr { - if err == nil { - t.Errorf("expected error, got nil (got=%q)", got) - } - } else { - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if string(got) != string(tc.want) { - t.Errorf("got %q; want %q", got, tc.want) - } - } - }) - } -} - -func TestGlobFSRelativePaths(t *testing.T) { - memfs := setupFS() - gfs := NewGlobFS(memfs, "docs/*.md") - entries, err := fs.ReadDir(gfs, "docs") - if err != nil { - t.Fatal(err) - } - got := basenames(entries) - want := []string{"guide.md"} - if !reflect.DeepEqual(got, want) { - t.Errorf("docs/*.md: got %v, want %v", got, want) - } -} - -func TestGlobFSNoMatchesOpen(t *testing.T) { - gfs := NewGlobFS(setupFS(), "*.xyz") - _, err := gfs.Open("main.go") - if err == nil { - t.Fatal("expected error when opening file with no matches") - } -} - -func TestGlobFSNoMatchesStat(t *testing.T) { - gfs := NewGlobFS(setupFS(), "*.xyz") - _, err := fs.Stat(gfs, "main.go") - if err == nil { - t.Fatal("expected error with no matches: stat") - } -} - -func TestGlobFSNoMatchesReadDir(t *testing.T) { - gfs := NewGlobFS(setupFS(), "*.xyz") - _, err := fs.ReadDir(gfs, "main.go") - if err == nil { - t.Fatal("expected error with no matches: readdir") - } -} - -func TestGlobFSNoMatchesReadFile(t *testing.T) { - gfs := NewGlobFS(setupFS(), "*.xyz") - _, err := fs.ReadFile(gfs, "main.go") - if err == nil { - t.Fatal("expected error with no matches: readfile") - } -} - -func TestGlobFS_IntegrationWithStdlib(t *testing.T) { - memfs := setupFS() - gfs := NewGlobFS(memfs, "*.go", "docs/guide.md") - // Use fs.WalkDir with our filtered FS - var walked []string - err := fs.WalkDir(gfs, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - walked = append(walked, path) - return nil - }) - if err != nil { - t.Fatal(err) - } - // Only files and dirs matching or containing matches should appear - for _, p := range walked { - if p == "." || p == "main.go" || p == "main_test.go" || p == "docs" || p == "docs/guide.md" { - continue - } - t.Errorf("WalkDir: unexpected path %q", p) - } -}