Skip to content

Commit

Permalink
feat: [CI-15681]: Enhance Drone GitHub Action Plugin with Workflow Ou…
Browse files Browse the repository at this point in the history
…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
Ompragash authored Jan 10, 2025
1 parent d3172c3 commit 11ed8ba
Show file tree
Hide file tree
Showing 14 changed files with 878 additions and 33 deletions.
72 changes: 72 additions & 0 deletions cache/cache.go
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))
}
44 changes: 44 additions & 0 deletions cloner/cache_cloner.go
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
}
26 changes: 26 additions & 0 deletions cloner/cloner.go
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
}
)
144 changes: 144 additions & 0 deletions cloner/default.go
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())
}
55 changes: 55 additions & 0 deletions cloner/default_test.go
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
}
35 changes: 35 additions & 0 deletions cloner/util.go
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
}
Loading

0 comments on commit 11ed8ba

Please sign in to comment.