├── .drone.yml ├── .gitignore ├── README.md ├── cache └── cache.go ├── cloner ├── cache_cloner.go ├── cloner.go ├── default.go ├── default_test.go ├── util.go └── util_test.go ├── cmd └── main.go ├── daemon ├── daemon.go ├── daemon_unix.go └── daemon_win.go ├── docker └── Dockerfile.linux.amd64 ├── go.mod ├── go.sum ├── pkg └── encoder │ └── encoder.go ├── plugin.go ├── scripts ├── build.sh └── docker.sh └── utils ├── env.go ├── parse.go ├── parse_test.go ├── workflow.go └── workflow_test.go /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | type: vm 3 | name: default 4 | 5 | platform: 6 | os: linux 7 | arch: amd64 8 | pool: 9 | use: ubuntu 10 | 11 | steps: 12 | - name: build 13 | image: golang:1.22.7 14 | commands: 15 | - go test ./... 16 | - sh scripts/build.sh 17 | 18 | - name: publish 19 | image: plugins/docker 20 | pull: if-not-exists 21 | settings: 22 | repo: plugins/github-actions 23 | dockerfile: docker/Dockerfile.linux.amd64 24 | username: 25 | from_secret: docker_username 26 | password: 27 | from_secret: docker_password 28 | auto_tag: true 29 | trigger: 30 | ref: 31 | - refs/heads/main 32 | - refs/tags/** 33 | - refs/pull/** 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | release 2 | coverage.out 3 | vendor 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # drone-github-action-plugin 2 | 3 | This plugin allows running github actions as a drone plugin. 4 | 5 | ## Build 6 | 7 | Build the binaries with the following commands: 8 | 9 | ```console 10 | export GOOS=linux 11 | export GOARCH=amd64 12 | export CGO_ENABLED=0 13 | export GO111MODULE=on 14 | 15 | go build -v -a -tags netgo -o release/linux/amd64/plugin ./cmd 16 | 17 | ``` 18 | 19 | ## Docker 20 | 21 | Build the Docker images with the following commands: 22 | 23 | ```console 24 | docker build \ 25 | --label org.label-schema.build-date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ 26 | --label org.label-schema.vcs-ref=$(git rev-parse --short HEAD) \ 27 | --file docker/Dockerfile.linux.amd64 --tag plugins/github-actions . 28 | 29 | ``` 30 | 31 | ## Plugin step usage 32 | 33 | Provide uses, with & env of github action to use in plugin step settings. Provide GITHUB_TOKEN as environment variable if it is required for an action. 34 | 35 | ```console 36 | steps: 37 | - name: github-action 38 | image: plugins/github-actions 39 | settings: 40 | uses: actions/hello-world-javascript-action@v1.1 41 | with: 42 | who-to-greet: Mona the Octocat 43 | env: 44 | hello: world 45 | 46 | ``` 47 | 48 | ## Running locally 49 | 50 | 1. If you are running it on mac locally & /var/run/docker.sock file does not exist, first run this command `ln -s ~/.docker/run/docker.sock /var/run/docker.sock` 51 | 2. Running actions/hello-world-javascript-action action locally via docker: 52 | 53 | ```console 54 | 55 | docker run --rm \ 56 | --privileged \ 57 | -v $(pwd):/drone \ 58 | -w /drone \ 59 | -e PLUGIN_USES="actions/hello-world-javascript-action@v1.1" \ 60 | -e PLUGIN_WITH="{\"who-to-greet\":\"Mona the Octocat\"}" \ 61 | -e PLUGIN_VERBOSE=true \ 62 | plugins/github-actions 63 | 64 | ``` 65 | -------------------------------------------------------------------------------- /cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/pkg/errors" 11 | "github.com/rogpeppe/go-internal/lockedfile" 12 | "golang.org/x/exp/slog" 13 | ) 14 | 15 | const ( 16 | completionMarkerFile = ".done" 17 | ) 18 | 19 | func Add(key string, addItem func() error) error { 20 | if err := os.MkdirAll(key, 0700); err != nil { 21 | return errors.Wrap(err, fmt.Sprintf("failed to create directory %s", key)) 22 | } 23 | 24 | lockFilepath := filepath.Join(key, ".started") 25 | slog.Debug("taking lock", "key", lockFilepath) 26 | lock, err := lockedfile.Create(lockFilepath) 27 | slog.Debug("took lock", "key", lockFilepath) 28 | 29 | if err != nil { 30 | return errors.Wrap(err, "failed to take file lock") 31 | } 32 | defer func() { 33 | if err := lock.Close(); err != nil { 34 | slog.Error("failed to release lock", "key", lockFilepath, "error", err) 35 | } 36 | slog.Debug("released lock", "key", lockFilepath) 37 | }() 38 | // If data is already present, return 39 | if _, err := os.Stat(filepath.Join(key, completionMarkerFile)); err == nil { 40 | return nil 41 | } 42 | 43 | if err := addItem(); err != nil { 44 | return errors.Wrap(err, fmt.Sprintf("failed to add item: %s to cache", key)) 45 | } 46 | 47 | integrityFpath := filepath.Join(key, completionMarkerFile) 48 | f, err := os.Create(integrityFpath) 49 | if err != nil { 50 | return errors.Wrap(err, fmt.Sprintf("failed to create integrity file: %s", integrityFpath)) 51 | } 52 | f.Close() 53 | 54 | return nil 55 | } 56 | 57 | // GetKeyName generate unique file path inside cache directory 58 | // based on name provided 59 | func GetKeyName(name string) string { 60 | return filepath.Join(getCacheDir(), sha(name)) 61 | } 62 | 63 | func getCacheDir() string { 64 | dir, _ := os.UserHomeDir() 65 | return filepath.Join(dir, ".cache") 66 | } 67 | 68 | func sha(s string) string { 69 | h := sha1.New() 70 | h.Write([]byte(s)) 71 | return hex.EncodeToString(h.Sum(nil)) 72 | } 73 | -------------------------------------------------------------------------------- /cloner/cache_cloner.go: -------------------------------------------------------------------------------- 1 | package cloner 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/drone-plugins/drone-github-actions/cache" 10 | "golang.org/x/exp/slog" 11 | ) 12 | 13 | func NewCache(cloner Cloner) *cacheCloner { 14 | return &cacheCloner{cloner: cloner} 15 | } 16 | 17 | type cacheCloner struct { 18 | cloner Cloner 19 | } 20 | 21 | // Clone method clones the repository & caches it if not present in cache already. 22 | func (c *cacheCloner) Clone(ctx context.Context, repo, ref, sha string) (string, error) { 23 | key := cache.GetKeyName(fmt.Sprintf("%s%s%s", repo, ref, sha)) 24 | codedir := filepath.Join(key, "data") 25 | 26 | cloneFn := func() error { 27 | // Remove stale data 28 | if err := os.RemoveAll(codedir); err != nil { 29 | slog.Error("cannot remove code directory", codedir, err) 30 | } 31 | 32 | if err := os.MkdirAll(codedir, 0700); err != nil { 33 | slog.Error("failed to create code directory", codedir, err) 34 | return err 35 | } 36 | return c.cloner.Clone(ctx, 37 | Params{Repo: repo, Ref: ref, Sha: sha, Dir: codedir}) 38 | } 39 | 40 | if err := cache.Add(key, cloneFn); err != nil { 41 | return "", err 42 | } 43 | return codedir, nil 44 | } 45 | -------------------------------------------------------------------------------- /cloner/cloner.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Harness Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package cloner provides support for cloning git repositories. 6 | package cloner 7 | 8 | import ( 9 | "context" 10 | ) 11 | 12 | type ( 13 | // Params provides clone params. 14 | Params struct { 15 | Repo string 16 | Ref string 17 | Sha string 18 | Dir string // Target clone directory. 19 | } 20 | 21 | // Cloner clones a repository. 22 | Cloner interface { 23 | // Clone a repository. 24 | Clone(context.Context, Params) error 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /cloner/default.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Harness Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cloner 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "io" 11 | "os" 12 | "regexp" 13 | "strings" 14 | "time" 15 | 16 | "github.com/cenkalti/backoff/v4" 17 | "github.com/go-git/go-git/v5" 18 | "github.com/go-git/go-git/v5/plumbing" 19 | "github.com/go-git/go-git/v5/plumbing/transport/http" 20 | ) 21 | 22 | const ( 23 | maxRetries = 3 24 | backoffInterval = time.Second * 1 25 | ) 26 | 27 | // New returns a new cloner. 28 | func New(depth int, stdout io.Writer) Cloner { 29 | c := &cloner{ 30 | depth: depth, 31 | stdout: stdout, 32 | } 33 | 34 | if token := os.Getenv("GITHUB_TOKEN"); token != "" { 35 | c.username = "token" 36 | c.password = token 37 | } 38 | return c 39 | } 40 | 41 | // NewDefault returns a cloner with default settings. 42 | func NewDefault() Cloner { 43 | return New(1, os.Stdout) 44 | } 45 | 46 | // default cloner using the built-in Git client. 47 | type cloner struct { 48 | depth int 49 | username string 50 | password string 51 | stdout io.Writer 52 | } 53 | 54 | // Clone the repository using the built-in Git client. 55 | func (c *cloner) Clone(ctx context.Context, params Params) error { 56 | opts := &git.CloneOptions{ 57 | RemoteName: "origin", 58 | Progress: c.stdout, 59 | URL: params.Repo, 60 | Tags: git.NoTags, 61 | } 62 | // set the reference name if provided 63 | if params.Ref != "" { 64 | opts.ReferenceName = plumbing.ReferenceName(expandRef(params.Ref)) 65 | } 66 | // set depth if cloning the head commit of a branch as 67 | // opposed to a specific commit sha 68 | if params.Sha == "" { 69 | opts.Depth = c.depth 70 | } 71 | if c.username != "" && c.password != "" { 72 | opts.Auth = &http.BasicAuth{ 73 | Username: c.username, 74 | Password: c.password, 75 | } 76 | } 77 | // clone the repository 78 | var ( 79 | r *git.Repository 80 | err error 81 | ) 82 | 83 | retryStrategy := backoff.NewExponentialBackOff() 84 | retryStrategy.InitialInterval = backoffInterval 85 | retryStrategy.MaxInterval = backoffInterval * 5 // Maximum delay 86 | retryStrategy.MaxElapsedTime = backoffInterval * 60 // Maximum time to retry (1min) 87 | 88 | b := backoff.WithMaxRetries(retryStrategy, uint64(maxRetries)) 89 | 90 | err = backoff.Retry(func() error { 91 | r, err = git.PlainClone(params.Dir, false, opts) 92 | if err == nil { 93 | return nil 94 | } 95 | if (errors.Is(plumbing.ErrReferenceNotFound, err) || matchRefNotFoundErr(err)) && 96 | !strings.HasPrefix(params.Ref, "refs/") { 97 | originalRefName := opts.ReferenceName 98 | // If params.Ref is provided without refs/*, then we are assuming it to either refs/heads/ or refs/tags. 99 | // Try clone again with inverse ref. 100 | if opts.ReferenceName.IsBranch() { 101 | opts.ReferenceName = plumbing.ReferenceName("refs/tags/" + params.Ref) 102 | } else if opts.ReferenceName.IsTag() { 103 | opts.ReferenceName = plumbing.ReferenceName("refs/heads/" + params.Ref) 104 | } else { 105 | return err // Return err if the reference name is invalid 106 | } 107 | 108 | r, err = git.PlainClone(params.Dir, false, opts) 109 | if err == nil { 110 | return nil 111 | } 112 | // Change reference name back to original 113 | opts.ReferenceName = originalRefName 114 | } 115 | return err 116 | }, b) 117 | 118 | // If error not nil, then return it 119 | if err != nil { 120 | return err 121 | } 122 | 123 | if params.Sha == "" { 124 | return nil 125 | } 126 | 127 | // checkout the sha 128 | w, err := r.Worktree() 129 | if err != nil { 130 | return err 131 | } 132 | return w.Checkout(&git.CheckoutOptions{ 133 | Hash: plumbing.NewHash(params.Sha), 134 | }) 135 | } 136 | 137 | func matchRefNotFoundErr(err error) bool { 138 | if err == nil { 139 | return false 140 | } 141 | pattern := `couldn't find remote ref.*` 142 | regex := regexp.MustCompile(pattern) 143 | return regex.MatchString(err.Error()) 144 | } 145 | -------------------------------------------------------------------------------- /cloner/default_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Harness Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cloner 6 | 7 | import ( 8 | "context" 9 | "os" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestClone(t *testing.T) { 17 | for name, tt := range map[string]struct { 18 | Err error 19 | URL, Ref string 20 | }{ 21 | "tag": { 22 | Err: nil, 23 | URL: "https://github.com/actions/checkout", 24 | Ref: "v2", 25 | }, 26 | "branch": { 27 | Err: nil, 28 | URL: "https://github.com/anchore/scan-action", 29 | Ref: "act-fails", 30 | }, 31 | "tag-special": { 32 | Err: nil, 33 | URL: "https://github.com/shubham149/drone-s3", 34 | Ref: "setup-node-and-dependencies+1.0.9", 35 | }, 36 | } { 37 | t.Run(name, func(t *testing.T) { 38 | c := NewDefault() 39 | err := c.Clone(context.Background(), Params{Repo: tt.URL, Ref: tt.Ref, Dir: testDir(t)}) 40 | if tt.Err != nil { 41 | assert.Error(t, err) 42 | assert.Equal(t, tt.Err, err) 43 | } else { 44 | assert.Empty(t, err) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func testDir(t *testing.T) string { 51 | basedir, err := os.MkdirTemp("", "act-test") 52 | require.NoError(t, err) 53 | t.Cleanup(func() { _ = os.RemoveAll(basedir) }) 54 | return basedir 55 | } 56 | -------------------------------------------------------------------------------- /cloner/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Harness Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cloner 6 | 7 | import ( 8 | "regexp" 9 | "strings" 10 | ) 11 | 12 | // regular expressions to test whether or not a string is 13 | // a sha1 or sha256 commit hash. 14 | var ( 15 | sha1 = regexp.MustCompile("^([a-f0-9]{40})$") 16 | sha256 = regexp.MustCompile("^([a-f0-9]{64})$") 17 | semver = regexp.MustCompile(`^v?((([0-9]+)(?:\.([0-9]+))?(?:\.([0-9]+))?(?:-([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)$`) 18 | ) 19 | 20 | // helper function returns true if the string is a commit hash. 21 | func isHash(s string) bool { 22 | return sha1.MatchString(s) || sha256.MatchString(s) 23 | } 24 | 25 | // helper function returns the branch name expanded to the 26 | // fully qualified reference path (e.g refs/heads/master). 27 | func expandRef(name string) string { 28 | if strings.HasPrefix(name, "refs/") { 29 | return name 30 | } 31 | if semver.MatchString(name) { 32 | return "refs/tags/" + name 33 | } 34 | return "refs/heads/" + name 35 | } 36 | -------------------------------------------------------------------------------- /cloner/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Harness Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cloner 6 | 7 | import "testing" 8 | 9 | func TestExpandRef(t *testing.T) { 10 | tests := []struct { 11 | name, prefix, after string 12 | }{ 13 | // branch references 14 | { 15 | after: "refs/heads/master", 16 | name: "master", 17 | prefix: "refs/heads", 18 | }, 19 | { 20 | after: "refs/heads/master", 21 | name: "master", 22 | prefix: "refs/heads/", 23 | }, 24 | // is already a ref 25 | { 26 | after: "refs/tags/v1.0.0", 27 | name: "refs/tags/v1.0.0", 28 | prefix: "refs/heads/", 29 | }, 30 | } 31 | for _, test := range tests { 32 | if got, want := expandRef(test.name), test.after; got != want { 33 | t.Errorf("Got reference %s, want %s", got, want) 34 | } 35 | } 36 | } 37 | 38 | func TestIsHash(t *testing.T) { 39 | tests := []struct { 40 | name string 41 | tag bool 42 | }{ 43 | { 44 | name: "aacad6eca956c3a340ae5cd5856aa9c4a3755408", 45 | tag: true, 46 | }, 47 | { 48 | name: "3da541559918a808c2402bba5012f6c60b27661c", 49 | tag: true, 50 | }, 51 | { 52 | name: "f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b", 53 | tag: true, 54 | }, 55 | // not a sha 56 | { 57 | name: "aacad6e", 58 | tag: false, 59 | }, 60 | { 61 | name: "master", 62 | tag: false, 63 | }, 64 | { 65 | name: "refs/heads/master", 66 | tag: false, 67 | }, 68 | { 69 | name: "issue/42", 70 | tag: false, 71 | }, 72 | { 73 | name: "feature/foo", 74 | tag: false, 75 | }, 76 | } 77 | for _, test := range tests { 78 | if got, want := isHash(test.name), test.tag; got != want { 79 | t.Errorf("Detected hash %v, want %v", got, want) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | plugin "github.com/drone-plugins/drone-github-actions" 8 | "github.com/drone-plugins/drone-github-actions/daemon" 9 | "github.com/drone-plugins/drone-github-actions/pkg/encoder" 10 | "github.com/joho/godotenv" 11 | "github.com/pkg/errors" 12 | "github.com/sirupsen/logrus" 13 | "github.com/urfave/cli" 14 | ) 15 | 16 | var ( 17 | version = "unknown" 18 | ) 19 | 20 | type genericMapType struct { 21 | m map[string]string 22 | strVal string 23 | } 24 | 25 | func (g *genericMapType) Set(value string) error { 26 | m := make(map[string]string) 27 | if err := json.Unmarshal([]byte(value), &m); err != nil { 28 | return err 29 | } 30 | g.m = m 31 | g.strVal = value 32 | return nil 33 | } 34 | 35 | func (g *genericMapType) String() string { 36 | return g.strVal 37 | } 38 | 39 | func main() { 40 | // Load env-file if it exists first 41 | if env := os.Getenv("PLUGIN_ENV_FILE"); env != "" { 42 | if err := godotenv.Load(env); err != nil { 43 | logrus.Fatal(err) 44 | } 45 | } 46 | 47 | app := cli.NewApp() 48 | app.Name = "drone github actions plugin" 49 | app.Usage = "drone github actions plugin" 50 | app.Action = run 51 | app.Version = version 52 | app.Flags = []cli.Flag{ 53 | cli.StringFlag{ 54 | Name: "action-name", 55 | Usage: "Github action name", 56 | EnvVar: "PLUGIN_USES", 57 | }, 58 | cli.StringFlag{ 59 | Name: "action-with", 60 | Usage: "Github action with", 61 | EnvVar: "PLUGIN_WITH", 62 | }, 63 | cli.StringFlag{ 64 | Name: "action-env", 65 | Usage: "Github action env", 66 | EnvVar: "PLUGIN_ENV", 67 | }, 68 | cli.BoolFlag{ 69 | Name: "action-verbose", 70 | Usage: "Github action enable verbose logging", 71 | EnvVar: "PLUGIN_VERBOSE", 72 | }, 73 | cli.StringFlag{ 74 | Name: "action-image", 75 | Usage: "Image to use for running github actions", 76 | Value: "node:16-buster-slim", 77 | EnvVar: "PLUGIN_ACTION_IMAGE", 78 | }, 79 | cli.StringFlag{ 80 | Name: "event-payload", 81 | Usage: "Webhook event payload", 82 | EnvVar: "PLUGIN_EVENT_PAYLOAD", 83 | }, 84 | cli.StringFlag{ 85 | Name: "actor", 86 | Usage: "User that triggered the event", 87 | EnvVar: "PLUGIN_ACTOR", 88 | }, 89 | 90 | // daemon flags 91 | cli.StringFlag{ 92 | Name: "docker.registry", 93 | Usage: "docker daemon registry", 94 | Value: "https://index.docker.io/v1/", 95 | EnvVar: "PLUGIN_DAEMON_REGISTRY", 96 | }, 97 | cli.StringFlag{ 98 | Name: "daemon.mirror", 99 | Usage: "docker daemon registry mirror", 100 | EnvVar: "PLUGIN_DAEMON_MIRROR", 101 | }, 102 | cli.StringFlag{ 103 | Name: "daemon.storage-driver", 104 | Usage: "docker daemon storage driver", 105 | EnvVar: "PLUGIN_DAEMON_STORAGE_DRIVER", 106 | }, 107 | cli.StringFlag{ 108 | Name: "daemon.storage-path", 109 | Usage: "docker daemon storage path", 110 | Value: "/var/lib/docker", 111 | EnvVar: "PLUGIN_DAEMON_STORAGE_PATH", 112 | }, 113 | cli.StringFlag{ 114 | Name: "daemon.bip", 115 | Usage: "docker daemon bride ip address", 116 | EnvVar: "PLUGIN_DAEMON_BIP", 117 | }, 118 | cli.StringFlag{ 119 | Name: "daemon.mtu", 120 | Usage: "docker daemon custom mtu setting", 121 | EnvVar: "PLUGIN_DAEMON_MTU", 122 | }, 123 | cli.StringSliceFlag{ 124 | Name: "daemon.dns", 125 | Usage: "docker daemon dns server", 126 | EnvVar: "PLUGIN_DAEMON_CUSTOM_DNS", 127 | }, 128 | cli.StringSliceFlag{ 129 | Name: "daemon.dns-search", 130 | Usage: "docker daemon dns search domains", 131 | EnvVar: "PLUGIN_DAEMON_CUSTOM_DNS_SEARCH", 132 | }, 133 | cli.BoolFlag{ 134 | Name: "daemon.insecure", 135 | Usage: "docker daemon allows insecure registries", 136 | EnvVar: "PLUGIN_DAEMON_INSECURE", 137 | }, 138 | cli.BoolFlag{ 139 | Name: "daemon.ipv6", 140 | Usage: "docker daemon IPv6 networking", 141 | EnvVar: "PLUGIN_DAEMON_IPV6", 142 | }, 143 | cli.BoolFlag{ 144 | Name: "daemon.experimental", 145 | Usage: "docker daemon Experimental mode", 146 | EnvVar: "PLUGIN_DAEMON_EXPERIMENTAL", 147 | }, 148 | cli.BoolFlag{ 149 | Name: "daemon.debug", 150 | Usage: "docker daemon executes in debug mode", 151 | EnvVar: "PLUGIN_DAEMON_DEBUG", 152 | }, 153 | cli.BoolFlag{ 154 | Name: "daemon.off", 155 | Usage: "don't start the docker daemon", 156 | EnvVar: "PLUGIN_DAEMON_OFF", 157 | }, 158 | } 159 | 160 | if err := app.Run(os.Args); err != nil { 161 | logrus.Fatal(err) 162 | } 163 | } 164 | 165 | func run(c *cli.Context) error { 166 | if c.String("action-name") == "" { 167 | return errors.New("uses attribute must be set") 168 | } 169 | 170 | actionWith, err := strToMap(c.String("action-with")) 171 | if err != nil { 172 | return errors.Wrap(err, "with attribute is not of map type with key & value as string") 173 | } 174 | actionEnv, err := strToMap(c.String("action-env")) 175 | if err != nil { 176 | return errors.Wrap(err, "env attribute is not of map type with key & value as string") 177 | } 178 | 179 | plugin := plugin.Plugin{ 180 | Action: plugin.Action{ 181 | Uses: c.String("action-name"), 182 | With: actionWith, 183 | Env: actionEnv, 184 | Verbose: c.Bool("action-verbose"), 185 | Image: c.String("action-image"), 186 | EventPayload: c.String("event-payload"), 187 | Actor: c.String("actor"), 188 | }, 189 | Daemon: daemon.Daemon{ 190 | Registry: c.String("docker.registry"), 191 | Mirror: c.String("daemon.mirror"), 192 | StorageDriver: c.String("daemon.storage-driver"), 193 | StoragePath: c.String("daemon.storage-path"), 194 | Insecure: c.Bool("daemon.insecure"), 195 | Disabled: c.Bool("daemon.off"), 196 | IPv6: c.Bool("daemon.ipv6"), 197 | Debug: c.Bool("daemon.debug"), 198 | Bip: c.String("daemon.bip"), 199 | DNS: c.StringSlice("daemon.dns"), 200 | DNSSearch: c.StringSlice("daemon.dns-search"), 201 | MTU: c.String("daemon.mtu"), 202 | Experimental: c.Bool("daemon.experimental"), 203 | }, 204 | } 205 | return plugin.Exec() 206 | } 207 | 208 | func strToMap(s string) (map[string]string, error) { 209 | m := make(map[string]string) 210 | if s == "" { 211 | return m, nil 212 | } 213 | 214 | if err := json.Unmarshal([]byte(s), &m); err != nil { 215 | m1 := make(map[string]interface{}) 216 | if e := json.Unmarshal([]byte(s), &m1); e != nil { 217 | return nil, e 218 | } 219 | 220 | for k, v := range m1 { 221 | m[k] = encoder.Encode(v) 222 | } 223 | } 224 | return m, nil 225 | } 226 | -------------------------------------------------------------------------------- /daemon/daemon.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "time" 7 | ) 8 | 9 | type Daemon struct { 10 | Registry string // Docker registry 11 | Mirror string // Docker registry mirror 12 | Insecure bool // Docker daemon enable insecure registries 13 | StorageDriver string // Docker daemon storage driver 14 | StoragePath string // Docker daemon storage path 15 | Disabled bool // Docker daemon is disabled (already running) 16 | Debug bool // Docker daemon started in debug mode 17 | Bip string // Docker daemon network bridge IP address 18 | DNS []string // Docker daemon dns server 19 | DNSSearch []string // Docker daemon dns search domain 20 | MTU string // Docker daemon mtu setting 21 | IPv6 bool // Docker daemon IPv6 networking 22 | Experimental bool // Docker daemon enable experimental mode 23 | } 24 | 25 | func StartDaemon(d Daemon) error { 26 | if !d.Disabled { 27 | startDaemon(d) 28 | } 29 | return waitForDaemon() 30 | } 31 | 32 | func waitForDaemon() error { 33 | // poll the docker daemon until it is started. This ensures the daemon is 34 | // ready to accept connections before we proceed. 35 | for i := 0; ; i++ { 36 | cmd := commandInfo() 37 | err := cmd.Run() 38 | if err == nil { 39 | break 40 | } 41 | if i == 15 { 42 | fmt.Println("Unable to reach Docker Daemon after 15 attempts.") 43 | return fmt.Errorf("failed to reach docker daemon after 15 attempts: %v", err) 44 | } 45 | time.Sleep(time.Second * 1) 46 | } 47 | return nil 48 | } 49 | 50 | // helper function to create the docker info command. 51 | func commandInfo() *exec.Cmd { 52 | return exec.Command(dockerExe, "info") 53 | } 54 | -------------------------------------------------------------------------------- /daemon/daemon_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package daemon 4 | 5 | import ( 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | ) 12 | 13 | const dockerExe = "/usr/local/bin/docker" 14 | const dockerdExe = "/usr/local/bin/dockerd" 15 | 16 | func startDaemon(daemon Daemon) { 17 | cmd := commandDaemon(daemon) 18 | if daemon.Debug { 19 | cmd.Stdout = os.Stdout 20 | cmd.Stderr = os.Stderr 21 | } else { 22 | cmd.Stdout = ioutil.Discard 23 | cmd.Stderr = ioutil.Discard 24 | } 25 | go func() { 26 | trace(cmd) 27 | cmd.Run() 28 | }() 29 | } 30 | 31 | // helper function to create the docker daemon command. 32 | func commandDaemon(daemon Daemon) *exec.Cmd { 33 | args := []string{ 34 | "--data-root", daemon.StoragePath, 35 | "--host=unix:///var/run/docker.sock", 36 | } 37 | 38 | if _, err := os.Stat("/etc/docker/default.json"); err == nil { 39 | args = append(args, "--seccomp-profile=/etc/docker/default.json") 40 | } 41 | 42 | if daemon.StorageDriver != "" { 43 | args = append(args, "-s", daemon.StorageDriver) 44 | } 45 | if daemon.Insecure && daemon.Registry != "" { 46 | args = append(args, "--insecure-registry", daemon.Registry) 47 | } 48 | if daemon.IPv6 { 49 | args = append(args, "--ipv6") 50 | } 51 | if len(daemon.Mirror) != 0 { 52 | args = append(args, "--registry-mirror", daemon.Mirror) 53 | } 54 | if len(daemon.Bip) != 0 { 55 | args = append(args, "--bip", daemon.Bip) 56 | } 57 | for _, dns := range daemon.DNS { 58 | args = append(args, "--dns", dns) 59 | } 60 | for _, dnsSearch := range daemon.DNSSearch { 61 | args = append(args, "--dns-search", dnsSearch) 62 | } 63 | if len(daemon.MTU) != 0 { 64 | args = append(args, "--mtu", daemon.MTU) 65 | } 66 | if daemon.Experimental { 67 | args = append(args, "--experimental") 68 | } 69 | return exec.Command(dockerdExe, args...) 70 | } 71 | 72 | // trace writes each command to stdout with the command wrapped in an xml 73 | // tag so that it can be extracted and displayed in the logs. 74 | func trace(cmd *exec.Cmd) { 75 | fmt.Fprintf(os.Stdout, "+ %s\n", strings.Join(cmd.Args, " ")) 76 | } 77 | -------------------------------------------------------------------------------- /daemon/daemon_win.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package daemon 4 | 5 | const dockerExe = "C:\\bin\\docker.exe" 6 | const dockerdExe = "" 7 | const dockerHome = "C:\\ProgramData\\docker\\" 8 | 9 | func startDaemon(daemon Daemon) { 10 | // this is a no-op on windows 11 | } 12 | -------------------------------------------------------------------------------- /docker/Dockerfile.linux.amd64: -------------------------------------------------------------------------------- 1 | FROM docker:dind 2 | 3 | ENV DOCKER_HOST=unix:///var/run/docker.sock 4 | 5 | RUN apk add --no-cache ca-certificates curl 6 | RUN curl -s https://raw.githubusercontent.com/nektos/act/master/install.sh | sh -s v0.2.61 7 | 8 | ADD release/linux/amd64/plugin /bin/ 9 | ENTRYPOINT ["/usr/local/bin/dockerd-entrypoint.sh", "/bin/plugin"] -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/drone-plugins/drone-github-actions 2 | 3 | go 1.22.7 4 | 5 | require ( 6 | github.com/buildkite/yaml v2.1.0+incompatible 7 | github.com/cenkalti/backoff/v4 v4.3.0 8 | github.com/ghodss/yaml v1.0.0 9 | github.com/go-git/go-git/v5 v5.13.1 10 | github.com/joho/godotenv v1.5.1 11 | github.com/pkg/errors v0.9.1 12 | github.com/rogpeppe/go-internal v1.13.1 13 | github.com/sirupsen/logrus v1.9.0 14 | github.com/stretchr/testify v1.10.0 15 | github.com/urfave/cli v1.22.12 16 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 17 | gopkg.in/yaml.v2 v2.4.0 18 | ) 19 | 20 | require ( 21 | dario.cat/mergo v1.0.0 // indirect 22 | github.com/Microsoft/go-winio v0.6.1 // indirect 23 | github.com/ProtonMail/go-crypto v1.1.3 // indirect 24 | github.com/cloudflare/circl v1.3.7 // indirect 25 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 26 | github.com/cyphar/filepath-securejoin v0.3.6 // indirect 27 | github.com/davecgh/go-spew v1.1.1 // indirect 28 | github.com/emirpasic/gods v1.18.1 // indirect 29 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 30 | github.com/go-git/go-billy/v5 v5.6.1 // indirect 31 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 32 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 33 | github.com/kevinburke/ssh_config v1.2.0 // indirect 34 | github.com/pjbgf/sha1cd v0.3.0 // indirect 35 | github.com/pmezard/go-difflib v1.0.0 // indirect 36 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 37 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 38 | github.com/skeema/knownhosts v1.3.0 // indirect 39 | github.com/xanzy/ssh-agent v0.3.3 // indirect 40 | golang.org/x/crypto v0.32.0 // indirect 41 | golang.org/x/mod v0.22.0 // indirect 42 | golang.org/x/net v0.34.0 // indirect 43 | golang.org/x/sync v0.10.0 // indirect 44 | golang.org/x/sys v0.29.0 // indirect 45 | golang.org/x/tools v0.29.0 // indirect 46 | gopkg.in/warnings.v0 v0.1.2 // indirect 47 | gopkg.in/yaml.v3 v3.0.1 // indirect 48 | ) 49 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 4 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 5 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 6 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 7 | github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= 8 | github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= 9 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 10 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 11 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 12 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 13 | github.com/buildkite/yaml v2.1.0+incompatible h1:xirI+ql5GzfikVNDmt+yeiXpf/v1Gt03qXTtT5WXdr8= 14 | github.com/buildkite/yaml v2.1.0+incompatible/go.mod h1:UoU8vbcwu1+vjZq01+KrpSeLBgQQIjL/H7Y6KwikUrI= 15 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 16 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 17 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 18 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 19 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 20 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 21 | github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= 22 | github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ= 27 | github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= 28 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 29 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 30 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 31 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 32 | github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= 33 | github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 34 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 35 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 36 | github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA= 37 | github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE= 38 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 39 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 40 | github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M= 41 | github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc= 42 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 43 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 44 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 45 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 46 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 47 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 48 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 49 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 50 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 51 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 52 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 53 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 54 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 55 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 56 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 57 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 58 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 59 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 60 | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 61 | github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 62 | github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 63 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 64 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 65 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 66 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 67 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 68 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 69 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 70 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 71 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 72 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 73 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 74 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 75 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 76 | github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= 77 | github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= 78 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 79 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 80 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 81 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 82 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 83 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 84 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 85 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 86 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 87 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 88 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 89 | github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8= 90 | github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= 91 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 92 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 93 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 94 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 95 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 96 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= 97 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= 98 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 99 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 100 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 101 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 102 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 103 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 104 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 105 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 106 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 107 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 108 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 109 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 110 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 111 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 112 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 113 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 114 | golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= 115 | golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= 116 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 117 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 118 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 119 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 120 | golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= 121 | golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= 122 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 123 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 124 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 125 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 126 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 127 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 128 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 129 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 130 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 131 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 132 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 133 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 134 | -------------------------------------------------------------------------------- /pkg/encoder/encoder.go: -------------------------------------------------------------------------------- 1 | package encoder 2 | 3 | import ( 4 | "encoding/base64" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/buildkite/yaml" 9 | json "github.com/ghodss/yaml" 10 | ) 11 | 12 | // Encode encodes an interface value as a string. This function 13 | // assumes all types were unmarshaled by the yaml.v2 library. 14 | // The yaml.v2 package only supports a subset of primitive types. 15 | func Encode(v interface{}) string { 16 | switch v := v.(type) { 17 | case string: 18 | return v 19 | case bool: 20 | return strconv.FormatBool(v) 21 | case int: 22 | return strconv.Itoa(v) 23 | case float64: 24 | return strconv.FormatFloat(v, 'g', -1, 64) 25 | case []byte: 26 | return base64.StdEncoding.EncodeToString(v) 27 | case []interface{}: 28 | return encodeSlice(v) 29 | default: 30 | return encodeMap(v) 31 | } 32 | } 33 | 34 | // helper function encodes a parameter in map format. 35 | func encodeMap(v interface{}) string { 36 | yml, _ := yaml.Marshal(v) 37 | out, _ := json.YAMLToJSON(yml) 38 | return string(out) 39 | } 40 | 41 | // helper function encodes a parameter in slice format. 42 | func encodeSlice(v interface{}) string { 43 | out, _ := yaml.Marshal(v) 44 | 45 | in := []string{} 46 | err := yaml.Unmarshal(out, &in) 47 | if err == nil { 48 | return strings.Join(in, ",") 49 | } 50 | out, _ = json.YAMLToJSON(out) 51 | return string(out) 52 | } 53 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/drone-plugins/drone-github-actions/cloner" 13 | "github.com/drone-plugins/drone-github-actions/daemon" 14 | "github.com/drone-plugins/drone-github-actions/utils" 15 | "github.com/pkg/errors" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | const ( 20 | envFile = "/tmp/action.env" 21 | secretFile = "/tmp/action.secrets" 22 | workflowFile = "/tmp/workflow.yml" 23 | eventPayloadFile = "/tmp/event.json" 24 | ) 25 | 26 | var ( 27 | secrets = []string{"GITHUB_TOKEN"} 28 | ) 29 | 30 | type ( 31 | Action struct { 32 | Uses string 33 | With map[string]string 34 | Env map[string]string 35 | Image string 36 | EventPayload string // Webhook event payload 37 | Actor string 38 | Verbose bool 39 | } 40 | 41 | Plugin struct { 42 | Action Action 43 | Daemon daemon.Daemon // Docker daemon configuration 44 | } 45 | ) 46 | 47 | // Exec executes the plugin step 48 | func (p Plugin) Exec() error { 49 | if err := daemon.StartDaemon(p.Daemon); err != nil { 50 | return err 51 | } 52 | 53 | ctx := context.Background() 54 | repoURL, ref, ok := utils.ParseLookup(p.Action.Uses) 55 | if !ok { 56 | logrus.Warnf("Invalid 'uses' format: %s", p.Action.Uses) 57 | } 58 | logrus.Infof("Parsed 'uses' string. Repo: %s, Ref: %s", repoURL, ref) 59 | 60 | // Clone the GH Action repository using `cloner` with parsed repo and ref 61 | clone := cloner.NewCache(cloner.NewDefault()) 62 | codedir, cloneErr := clone.Clone(ctx, repoURL, ref, "") 63 | if cloneErr != nil { 64 | logrus.Warnf("Failed to clone GH Action: %v", cloneErr) 65 | } else { 66 | logrus.Infof("Successfully cloned GH Action to %s", codedir) 67 | } 68 | 69 | outputFile := os.Getenv("DRONE_OUTPUT") 70 | outputVars := []string{} 71 | 72 | if codedir != "" { 73 | var err error 74 | outputVars, err = utils.ParseActionOutputs(codedir) 75 | if err != nil { 76 | logrus.Warnf("Could not parse action.yml outputs from %s: %v", codedir, err) 77 | } 78 | } 79 | 80 | if len(outputVars) == 0 { 81 | logrus.Infof("No outputs were found in action.yml for repo: %s", repoURL) 82 | } 83 | 84 | if err := utils.CreateWorkflowFile(workflowFile, p.Action.Uses, 85 | p.Action.With, p.Action.Env, outputFile, outputVars); err != nil { 86 | return err 87 | } 88 | 89 | if err := utils.CreateEnvAndSecretFile(envFile, secretFile, secrets); err != nil { 90 | return err 91 | } 92 | 93 | outputFilePath := GetDirPath(outputFile) 94 | containerOptions := fmt.Sprintf("-v=%s:%s", outputFilePath, outputFilePath) 95 | 96 | cmdArgs := []string{ 97 | "-W", 98 | workflowFile, 99 | "-P", 100 | fmt.Sprintf("ubuntu-latest=%s", p.Action.Image), 101 | "--secret-file", 102 | secretFile, 103 | "--env-file", 104 | envFile, 105 | "-b", 106 | "--detect-event", 107 | "--container-options", 108 | fmt.Sprintf("\"%s\"", containerOptions), 109 | } 110 | 111 | // optional arguments 112 | if p.Action.Actor != "" { 113 | cmdArgs = append(cmdArgs, "--actor") 114 | cmdArgs = append(cmdArgs, p.Action.Actor) 115 | } 116 | 117 | if p.Action.EventPayload != "" { 118 | if err := ioutil.WriteFile(eventPayloadFile, []byte(p.Action.EventPayload), 0644); err != nil { 119 | return errors.Wrap(err, "failed to write event payload to file") 120 | } 121 | 122 | cmdArgs = append(cmdArgs, "--eventpath", eventPayloadFile) 123 | } 124 | 125 | if p.Action.Verbose { 126 | cmdArgs = append(cmdArgs, "-v") 127 | } 128 | 129 | cmd := exec.Command("act", cmdArgs...) 130 | cmd.Stdout = os.Stdout 131 | cmd.Stderr = os.Stderr 132 | trace(cmd) 133 | 134 | err := cmd.Run() 135 | if err != nil { 136 | return err 137 | } 138 | return nil 139 | } 140 | 141 | // trace writes each command to stdout with the command wrapped in an xml 142 | // tag so that it can be extracted and displayed in the logs. 143 | func trace(cmd *exec.Cmd) { 144 | fmt.Fprintf(os.Stdout, "+ %s\n", strings.Join(cmd.Args, " ")) 145 | } 146 | 147 | func GetDirPath(filePath string) string { 148 | return filepath.Dir(filePath) 149 | } 150 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # force go modules 4 | export GOPATH="" 5 | 6 | # disable cgo 7 | export CGO_ENABLED=0 8 | 9 | set -e 10 | set -x 11 | 12 | # linux 13 | GOOS=linux GOARCH=amd64 go build -o release/linux/amd64/plugin ./cmd -------------------------------------------------------------------------------- /scripts/docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # force go modules 4 | export GOPATH="" 5 | 6 | # disable cgo 7 | export CGO_ENABLED=0 8 | 9 | set -e 10 | set -x 11 | 12 | # linux 13 | GOOS=linux GOARCH=amd64 go build -o release/linux/amd64/plugin ./cmd 14 | 15 | docker build -f docker/Dockerfile.linux.amd64 -t plugins/github-actions . -------------------------------------------------------------------------------- /utils/env.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/joho/godotenv" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func CreateEnvAndSecretFile(envFile, secretFile string, secrets []string) error { 12 | envVars := getEnvVars() 13 | 14 | actionEnvVars := make(map[string]string) 15 | for key, val := range envVars { 16 | if !strings.HasPrefix(key, "PLUGIN_") && !Exists(secrets, key) { 17 | actionEnvVars[key] = val 18 | } 19 | } 20 | 21 | secretEnvVars := make(map[string]string) 22 | for _, secretName := range secrets { 23 | if os.Getenv(secretName) != "" { 24 | secretEnvVars[secretName] = os.Getenv(secretName) 25 | } 26 | } 27 | 28 | if err := godotenv.Write(actionEnvVars, envFile); err != nil { 29 | return errors.Wrap(err, "failed to write environment variables file") 30 | } 31 | if err := godotenv.Write(secretEnvVars, secretFile); err != nil { 32 | return errors.Wrap(err, "failed to write secret variables file") 33 | } 34 | return nil 35 | } 36 | 37 | // Return environment variables set in a map format 38 | func getEnvVars() map[string]string { 39 | m := make(map[string]string) 40 | for _, e := range os.Environ() { 41 | if i := strings.Index(e, "="); i >= 0 { 42 | m[e[:i]] = e[i+1:] 43 | } 44 | } 45 | return m 46 | } 47 | 48 | func Exists(slice []string, val string) bool { 49 | for _, item := range slice { 50 | if item == val { 51 | return true 52 | } 53 | } 54 | return false 55 | } 56 | -------------------------------------------------------------------------------- /utils/parse.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "strings" 10 | 11 | "golang.org/x/exp/slog" 12 | 13 | "github.com/sirupsen/logrus" 14 | "gopkg.in/yaml.v2" 15 | ) 16 | 17 | type GHActionSpec struct { 18 | Outputs map[string]interface{} `yaml:"outputs,omitempty"` 19 | } 20 | 21 | // ParseActionOutputs locates `action.yml` or `action.yaml` in `root` and returns all top-level outputs. 22 | func ParseActionOutputs(root string) ([]string, error) { 23 | ymlPath := filepath.Join(root, "action.yml") 24 | yamlPath := filepath.Join(root, "action.yaml") 25 | 26 | var actionFile string 27 | switch { 28 | case fileExists(ymlPath): 29 | actionFile = ymlPath 30 | case fileExists(yamlPath): 31 | actionFile = yamlPath 32 | default: 33 | logrus.Warnf("action.yml or action.yaml not found in %s. Skipping output variable processing.", root) 34 | return []string{}, nil 35 | } 36 | 37 | raw, err := os.ReadFile(actionFile) 38 | if err != nil { 39 | return nil, fmt.Errorf("failed to read action file: %w", err) 40 | } 41 | 42 | var spec GHActionSpec 43 | if err := yaml.Unmarshal(raw, &spec); err != nil { 44 | return nil, fmt.Errorf("failed to parse action.yml: %w", err) 45 | } 46 | 47 | keys := make([]string, 0, len(spec.Outputs)) 48 | for k := range spec.Outputs { 49 | keys = append(keys, k) 50 | } 51 | return keys, nil 52 | } 53 | 54 | // fileExists checks if the given path exists and is a file. 55 | func fileExists(path string) bool { 56 | info, err := os.Stat(path) 57 | if err != nil { 58 | return false 59 | } 60 | return !info.IsDir() 61 | } 62 | 63 | // ParseLookup parses the step string and returns the 64 | // associated repository and ref. 65 | func ParseLookup(s string) (repo string, ref string, ok bool) { 66 | org, repo, _, ref, err := parseActionName(s) 67 | if err == nil { 68 | url := fmt.Sprintf("https://github.com/%s/%s", org, repo) 69 | slog.Debug(fmt.Sprintf("parsed repo: %s, ref: %s", url, ref)) 70 | return url, ref, true 71 | } 72 | 73 | slog.Warn(fmt.Sprintf("failed to parse action name: %s with err: %v", s, err)) 74 | if !strings.HasPrefix(s, "https://github.com") { 75 | s, _ = url.JoinPath("https://github.com", s) 76 | } 77 | 78 | slog.Debug("parsed repo", s) 79 | if parts := strings.SplitN(s, "@", 2); len(parts) == 2 { 80 | return parts[0], parts[1], true 81 | } 82 | return s, "", true 83 | } 84 | 85 | func parseActionName(action string) (org, repo, path, ref string, err error) { 86 | r := regexp.MustCompile(`^([^/@]+)/([^/@]+)(/([^@]*))?(@(.*))?$`) 87 | matches := r.FindStringSubmatch(action) 88 | if len(matches) < 7 || matches[6] == "" { 89 | err = fmt.Errorf("invalid action name: %s", action) 90 | return 91 | } 92 | org = matches[1] 93 | repo = matches[2] 94 | path = matches[4] 95 | ref = matches[6] 96 | return 97 | } 98 | -------------------------------------------------------------------------------- /utils/parse_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestParseActionOutputs(t *testing.T) { 12 | testDir := t.TempDir() 13 | 14 | // Valid action.yml file 15 | validAction := ` 16 | outputs: 17 | var1: 18 | description: "A sample output variable" 19 | var-2: 20 | description: "Another variable"` 21 | err := os.WriteFile(filepath.Join(testDir, "action.yml"), []byte(validAction), 0644) 22 | assert.NoError(t, err) 23 | 24 | // Debugging file creation 25 | _, statErr := os.Stat(filepath.Join(testDir, "action.yml")) 26 | assert.NoError(t, statErr, "action.yml file not created") 27 | 28 | outputs, err := ParseActionOutputs(testDir) 29 | assert.NoError(t, err) 30 | assert.ElementsMatch(t, outputs, []string{"var1", "var-2"}) 31 | 32 | // Invalid action.yml 33 | invalidAction := `invalid_yaml` 34 | err = os.WriteFile(filepath.Join(testDir, "action.yml"), []byte(invalidAction), 0644) 35 | assert.NoError(t, err) 36 | 37 | _, err = ParseActionOutputs(testDir) 38 | assert.Error(t, err) 39 | 40 | // No action.yml or action.yaml 41 | os.Remove(filepath.Join(testDir, "action.yml")) 42 | outputs, err = ParseActionOutputs(testDir) 43 | assert.NoError(t, err) 44 | assert.Empty(t, outputs) 45 | } 46 | -------------------------------------------------------------------------------- /utils/workflow.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/sirupsen/logrus" 10 | "gopkg.in/yaml.v2" 11 | ) 12 | 13 | type workflow struct { 14 | Name string `yaml:"name"` 15 | On string `yaml:"on"` 16 | Jobs map[string]job `yaml:"jobs"` 17 | } 18 | 19 | type job struct { 20 | Name string `yaml:"name"` 21 | RunsOn string `yaml:"runs-on"` 22 | Steps []step `yaml:"steps"` 23 | } 24 | 25 | type step struct { 26 | Id string `yaml:"id,omitempty"` 27 | Name string `yaml:"name,omitempty"` 28 | Uses string `yaml:"uses"` 29 | Run string `yaml:"run,omitempty"` 30 | With map[string]string `yaml:"with"` 31 | Env map[string]string `yaml:"env"` 32 | Shell string `yaml:"shell,omitempty"` 33 | If string `yaml:"if,omitempty"` 34 | } 35 | 36 | const ( 37 | stepId = "stepIdentifier" 38 | workflowEvent = "push" 39 | workflowName = "drone-github-action" 40 | jobName = "action" 41 | runsOnImage = "ubuntu-latest" 42 | ) 43 | 44 | func CreateWorkflowFile(ymlFile string, action string, 45 | with map[string]string, env map[string]string, outputFile string, outputVars []string) error { 46 | j := job{ 47 | Name: jobName, 48 | RunsOn: runsOnImage, 49 | Steps: []step{ 50 | { 51 | Id: stepId, 52 | Uses: action, 53 | With: with, 54 | Env: env, 55 | }, 56 | setOutputVariables(stepId, outputFile, outputVars), 57 | }, 58 | } 59 | wf := &workflow{ 60 | Name: workflowName, 61 | On: getWorkflowEvent(), 62 | Jobs: map[string]job{ 63 | jobName: j, 64 | }, 65 | } 66 | 67 | out, err := yaml.Marshal(&wf) 68 | if err != nil { 69 | return errors.Wrap(err, "failed to create action workflow yml") 70 | } 71 | 72 | if err = ioutil.WriteFile(ymlFile, out, 0644); err != nil { 73 | return errors.Wrap(err, "failed to write yml workflow file") 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func getWorkflowEvent() string { 80 | buildEvent := os.Getenv("DRONE_BUILD_EVENT") 81 | if buildEvent == "push" || buildEvent == "pull_request" || buildEvent == "tag" { 82 | return buildEvent 83 | } 84 | return "custom" 85 | } 86 | 87 | func setOutputVariables(prevStepId, outputFile string, outputVars []string) step { 88 | skip := len(outputFile) == 0 || len(outputVars) == 0 89 | if skip { 90 | logrus.Infof("No output variables detected in action.yml; skipping output file generation.") 91 | } 92 | 93 | cmd := "" 94 | for _, outputVar := range outputVars { 95 | cmd += fmt.Sprintf("%s=${{ steps.%s.outputs.%s }}\n", outputVar, prevStepId, outputVar) 96 | } 97 | 98 | cmd = fmt.Sprintf("echo \"%s\" > %s", cmd, outputFile) 99 | s := step{ 100 | Name: "output variables", 101 | Run: cmd, 102 | If: fmt.Sprintf("%t", !skip), 103 | } 104 | return s 105 | } 106 | -------------------------------------------------------------------------------- /utils/workflow_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCreateWorkflowFile(t *testing.T) { 12 | testDir := t.TempDir() 13 | workflowFile := testDir + "/workflow.yml" 14 | outputFile := testDir + "/output" 15 | 16 | action := "some-action@v1" 17 | with := map[string]string{"input1": "value1"} 18 | env := map[string]string{"VAR": "value"} 19 | outputVars := []string{"out1", "out-2"} 20 | 21 | // With output variables 22 | err := CreateWorkflowFile(workflowFile, action, with, env, outputFile, outputVars) 23 | assert.NoError(t, err) 24 | 25 | content, err := os.ReadFile(workflowFile) 26 | assert.NoError(t, err) 27 | 28 | // Check general structure 29 | assert.Contains(t, string(content), "uses: some-action@v1") 30 | assert.Contains(t, string(content), "steps:") 31 | assert.Contains(t, string(content), "id: stepIdentifier") 32 | 33 | // Check the `run` command 34 | assert.Contains(t, string(content), "out1=${{ steps.stepIdentifier.outputs.out1 }}") 35 | assert.Contains(t, string(content), "out-2=${{ steps.stepIdentifier.outputs.out-2 }}") 36 | assert.Contains(t, string(content), fmt.Sprintf("> %s", outputFile)) 37 | 38 | // Without output variables 39 | err = CreateWorkflowFile(workflowFile, action, with, env, outputFile, []string{}) 40 | assert.NoError(t, err) 41 | content, err = os.ReadFile(workflowFile) 42 | assert.NoError(t, err) 43 | assert.Contains(t, string(content), "name: output variables") 44 | assert.Contains(t, string(content), "run: echo \"\" >") 45 | assert.Contains(t, string(content), "if: \"false\"") 46 | } 47 | 48 | func TestSetOutputVariables(t *testing.T) { 49 | outputVars := []string{"var1", "var2"} 50 | prevStepId := "prevStep" 51 | outputFile := "/tmp/output" 52 | 53 | // With output variables 54 | step := setOutputVariables(prevStepId, outputFile, outputVars) 55 | assert.Equal(t, "output variables", step.Name) 56 | assert.Contains(t, step.Run, "var1=${{ steps.prevStep.outputs.var1 }}") 57 | assert.Contains(t, step.Run, "var2=${{ steps.prevStep.outputs.var2 }}") 58 | 59 | // No output variables 60 | step = setOutputVariables(prevStepId, outputFile, []string{}) 61 | assert.Equal(t, "output variables", step.Name) 62 | assert.Contains(t, step.Run, "echo \"\" > /tmp/output") 63 | assert.Contains(t, step.If, "false") 64 | } 65 | --------------------------------------------------------------------------------