-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: [CI-15681]: Enhance Drone GitHub Action Plugin with Workflow Ou…
…tput Parsing (#18) * feat: [CI-15681]: Enhance Drone GitHub Actions Plugin with Workflow Output Parsing * formatted parse_test.go * Removed 'drone/plugin' dependencies to reduce the binary size and copied the relevant code to this repo * Removed windows code * Updated plugin.go
- Loading branch information
Showing
14 changed files
with
878 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package cache | ||
|
||
import ( | ||
"crypto/sha1" | ||
"encoding/hex" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/pkg/errors" | ||
"github.com/rogpeppe/go-internal/lockedfile" | ||
"golang.org/x/exp/slog" | ||
) | ||
|
||
const ( | ||
completionMarkerFile = ".done" | ||
) | ||
|
||
func Add(key string, addItem func() error) error { | ||
if err := os.MkdirAll(key, 0700); err != nil { | ||
return errors.Wrap(err, fmt.Sprintf("failed to create directory %s", key)) | ||
} | ||
|
||
lockFilepath := filepath.Join(key, ".started") | ||
slog.Debug("taking lock", "key", lockFilepath) | ||
lock, err := lockedfile.Create(lockFilepath) | ||
slog.Debug("took lock", "key", lockFilepath) | ||
|
||
if err != nil { | ||
return errors.Wrap(err, "failed to take file lock") | ||
} | ||
defer func() { | ||
if err := lock.Close(); err != nil { | ||
slog.Error("failed to release lock", "key", lockFilepath, "error", err) | ||
} | ||
slog.Debug("released lock", "key", lockFilepath) | ||
}() | ||
// If data is already present, return | ||
if _, err := os.Stat(filepath.Join(key, completionMarkerFile)); err == nil { | ||
return nil | ||
} | ||
|
||
if err := addItem(); err != nil { | ||
return errors.Wrap(err, fmt.Sprintf("failed to add item: %s to cache", key)) | ||
} | ||
|
||
integrityFpath := filepath.Join(key, completionMarkerFile) | ||
f, err := os.Create(integrityFpath) | ||
if err != nil { | ||
return errors.Wrap(err, fmt.Sprintf("failed to create integrity file: %s", integrityFpath)) | ||
} | ||
f.Close() | ||
|
||
return nil | ||
} | ||
|
||
// GetKeyName generate unique file path inside cache directory | ||
// based on name provided | ||
func GetKeyName(name string) string { | ||
return filepath.Join(getCacheDir(), sha(name)) | ||
} | ||
|
||
func getCacheDir() string { | ||
dir, _ := os.UserHomeDir() | ||
return filepath.Join(dir, ".cache") | ||
} | ||
|
||
func sha(s string) string { | ||
h := sha1.New() | ||
h.Write([]byte(s)) | ||
return hex.EncodeToString(h.Sum(nil)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package cloner | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/drone-plugins/drone-github-actions/cache" | ||
"golang.org/x/exp/slog" | ||
) | ||
|
||
func NewCache(cloner Cloner) *cacheCloner { | ||
return &cacheCloner{cloner: cloner} | ||
} | ||
|
||
type cacheCloner struct { | ||
cloner Cloner | ||
} | ||
|
||
// Clone method clones the repository & caches it if not present in cache already. | ||
func (c *cacheCloner) Clone(ctx context.Context, repo, ref, sha string) (string, error) { | ||
key := cache.GetKeyName(fmt.Sprintf("%s%s%s", repo, ref, sha)) | ||
codedir := filepath.Join(key, "data") | ||
|
||
cloneFn := func() error { | ||
// Remove stale data | ||
if err := os.RemoveAll(codedir); err != nil { | ||
slog.Error("cannot remove code directory", codedir, err) | ||
} | ||
|
||
if err := os.MkdirAll(codedir, 0700); err != nil { | ||
slog.Error("failed to create code directory", codedir, err) | ||
return err | ||
} | ||
return c.cloner.Clone(ctx, | ||
Params{Repo: repo, Ref: ref, Sha: sha, Dir: codedir}) | ||
} | ||
|
||
if err := cache.Add(key, cloneFn); err != nil { | ||
return "", err | ||
} | ||
return codedir, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// Copyright 2022 Harness Inc. All rights reserved. | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
// Package cloner provides support for cloning git repositories. | ||
package cloner | ||
|
||
import ( | ||
"context" | ||
) | ||
|
||
type ( | ||
// Params provides clone params. | ||
Params struct { | ||
Repo string | ||
Ref string | ||
Sha string | ||
Dir string // Target clone directory. | ||
} | ||
|
||
// Cloner clones a repository. | ||
Cloner interface { | ||
// Clone a repository. | ||
Clone(context.Context, Params) error | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
// Copyright 2022 Harness Inc. All rights reserved. | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package cloner | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"io" | ||
"os" | ||
"regexp" | ||
"strings" | ||
"time" | ||
|
||
"github.com/cenkalti/backoff/v4" | ||
"github.com/go-git/go-git/v5" | ||
"github.com/go-git/go-git/v5/plumbing" | ||
"github.com/go-git/go-git/v5/plumbing/transport/http" | ||
) | ||
|
||
const ( | ||
maxRetries = 3 | ||
backoffInterval = time.Second * 1 | ||
) | ||
|
||
// New returns a new cloner. | ||
func New(depth int, stdout io.Writer) Cloner { | ||
c := &cloner{ | ||
depth: depth, | ||
stdout: stdout, | ||
} | ||
|
||
if token := os.Getenv("GITHUB_TOKEN"); token != "" { | ||
c.username = "token" | ||
c.password = token | ||
} | ||
return c | ||
} | ||
|
||
// NewDefault returns a cloner with default settings. | ||
func NewDefault() Cloner { | ||
return New(1, os.Stdout) | ||
} | ||
|
||
// default cloner using the built-in Git client. | ||
type cloner struct { | ||
depth int | ||
username string | ||
password string | ||
stdout io.Writer | ||
} | ||
|
||
// Clone the repository using the built-in Git client. | ||
func (c *cloner) Clone(ctx context.Context, params Params) error { | ||
opts := &git.CloneOptions{ | ||
RemoteName: "origin", | ||
Progress: c.stdout, | ||
URL: params.Repo, | ||
Tags: git.NoTags, | ||
} | ||
// set the reference name if provided | ||
if params.Ref != "" { | ||
opts.ReferenceName = plumbing.ReferenceName(expandRef(params.Ref)) | ||
} | ||
// set depth if cloning the head commit of a branch as | ||
// opposed to a specific commit sha | ||
if params.Sha == "" { | ||
opts.Depth = c.depth | ||
} | ||
if c.username != "" && c.password != "" { | ||
opts.Auth = &http.BasicAuth{ | ||
Username: c.username, | ||
Password: c.password, | ||
} | ||
} | ||
// clone the repository | ||
var ( | ||
r *git.Repository | ||
err error | ||
) | ||
|
||
retryStrategy := backoff.NewExponentialBackOff() | ||
retryStrategy.InitialInterval = backoffInterval | ||
retryStrategy.MaxInterval = backoffInterval * 5 // Maximum delay | ||
retryStrategy.MaxElapsedTime = backoffInterval * 60 // Maximum time to retry (1min) | ||
|
||
b := backoff.WithMaxRetries(retryStrategy, uint64(maxRetries)) | ||
|
||
err = backoff.Retry(func() error { | ||
r, err = git.PlainClone(params.Dir, false, opts) | ||
if err == nil { | ||
return nil | ||
} | ||
if (errors.Is(plumbing.ErrReferenceNotFound, err) || matchRefNotFoundErr(err)) && | ||
!strings.HasPrefix(params.Ref, "refs/") { | ||
originalRefName := opts.ReferenceName | ||
// If params.Ref is provided without refs/*, then we are assuming it to either refs/heads/ or refs/tags. | ||
// Try clone again with inverse ref. | ||
if opts.ReferenceName.IsBranch() { | ||
opts.ReferenceName = plumbing.ReferenceName("refs/tags/" + params.Ref) | ||
} else if opts.ReferenceName.IsTag() { | ||
opts.ReferenceName = plumbing.ReferenceName("refs/heads/" + params.Ref) | ||
} else { | ||
return err // Return err if the reference name is invalid | ||
} | ||
|
||
r, err = git.PlainClone(params.Dir, false, opts) | ||
if err == nil { | ||
return nil | ||
} | ||
// Change reference name back to original | ||
opts.ReferenceName = originalRefName | ||
} | ||
return err | ||
}, b) | ||
|
||
// If error not nil, then return it | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if params.Sha == "" { | ||
return nil | ||
} | ||
|
||
// checkout the sha | ||
w, err := r.Worktree() | ||
if err != nil { | ||
return err | ||
} | ||
return w.Checkout(&git.CheckoutOptions{ | ||
Hash: plumbing.NewHash(params.Sha), | ||
}) | ||
} | ||
|
||
func matchRefNotFoundErr(err error) bool { | ||
if err == nil { | ||
return false | ||
} | ||
pattern := `couldn't find remote ref.*` | ||
regex := regexp.MustCompile(pattern) | ||
return regex.MatchString(err.Error()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// Copyright 2022 Harness Inc. All rights reserved. | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package cloner | ||
|
||
import ( | ||
"context" | ||
"os" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestClone(t *testing.T) { | ||
for name, tt := range map[string]struct { | ||
Err error | ||
URL, Ref string | ||
}{ | ||
"tag": { | ||
Err: nil, | ||
URL: "https://github.com/actions/checkout", | ||
Ref: "v2", | ||
}, | ||
"branch": { | ||
Err: nil, | ||
URL: "https://github.com/anchore/scan-action", | ||
Ref: "act-fails", | ||
}, | ||
"tag-special": { | ||
Err: nil, | ||
URL: "https://github.com/shubham149/drone-s3", | ||
Ref: "setup-node-and-dependencies+1.0.9", | ||
}, | ||
} { | ||
t.Run(name, func(t *testing.T) { | ||
c := NewDefault() | ||
err := c.Clone(context.Background(), Params{Repo: tt.URL, Ref: tt.Ref, Dir: testDir(t)}) | ||
if tt.Err != nil { | ||
assert.Error(t, err) | ||
assert.Equal(t, tt.Err, err) | ||
} else { | ||
assert.Empty(t, err) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func testDir(t *testing.T) string { | ||
basedir, err := os.MkdirTemp("", "act-test") | ||
require.NoError(t, err) | ||
t.Cleanup(func() { _ = os.RemoveAll(basedir) }) | ||
return basedir | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
// Copyright 2022 Harness Inc. All rights reserved. | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package cloner | ||
|
||
import ( | ||
"regexp" | ||
"strings" | ||
) | ||
|
||
// regular expressions to test whether or not a string is | ||
// a sha1 or sha256 commit hash. | ||
var ( | ||
sha1 = regexp.MustCompile("^([a-f0-9]{40})$") | ||
sha256 = regexp.MustCompile("^([a-f0-9]{64})$") | ||
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-]+)*))?)$`) | ||
) | ||
|
||
// helper function returns true if the string is a commit hash. | ||
func isHash(s string) bool { | ||
return sha1.MatchString(s) || sha256.MatchString(s) | ||
} | ||
|
||
// helper function returns the branch name expanded to the | ||
// fully qualified reference path (e.g refs/heads/master). | ||
func expandRef(name string) string { | ||
if strings.HasPrefix(name, "refs/") { | ||
return name | ||
} | ||
if semver.MatchString(name) { | ||
return "refs/tags/" + name | ||
} | ||
return "refs/heads/" + name | ||
} |
Oops, something went wrong.