Skip to content

Commit

Permalink
Add output variables support for gha and bitrise (#9)
Browse files Browse the repository at this point in the history
* add output variables support for gha and bitrise plugins

* change

* change

* review changes
  • Loading branch information
raghavharness authored Feb 24, 2023
1 parent 19bfe2f commit 1c16468
Show file tree
Hide file tree
Showing 12 changed files with 259 additions and 36 deletions.
4 changes: 4 additions & 0 deletions cloner/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
var (
sha1 = regexp.MustCompile("^([a-f0-9]{40})$")
sha256 = regexp.MustCompile("^([a-f0-9]{64})$")
semver = regexp.MustCompile(`^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-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.
Expand All @@ -27,5 +28,8 @@ func expandRef(name string) string {
if strings.HasPrefix(name, "refs/") {
return name
}
if semver.MatchString(name) {
return "refs/tags/" + name
}
return "refs/heads/" + name
}
51 changes: 33 additions & 18 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ import (
)

var (
name string // plugin name
repo string // plugin repository
ref string // plugin repository reference
sha string // plugin repository commit
kind string // plugin kind (action, bitrise, harness)
name string // plugin name
repo string // plugin repository
ref string // plugin repository reference
sha string // plugin repository commit
kind string // plugin kind (action, bitrise, harness)
outputfile string // plugin outputfile
)

func main() {
Expand All @@ -37,26 +38,24 @@ func main() {
flag.StringVar(&ref, "ref", "", "plugin reference")
flag.StringVar(&sha, "sha", "", "plugin commit")
flag.StringVar(&kind, "kind", "", "plugin kind")
flag.StringVar(&outputfile, "outputfile", "", "filepath to store output variables")
flag.Parse()

if kind == "action" {
execer := github.Execer{
Name: name,
Stdout: os.Stdout,
Stderr: os.Stderr,
Environ: os.Environ(),
}
if err := execer.Exec(ctx); err != nil {
log.Error("action step failed", err)
os.Exit(1)
// the user may specific the action plugin alias instead
// of the git repository. We are able to lookup the plugin
// by alias to find the corresponding repository and ref.
if repo == "" && kind == "action" {
repo_, ref_, ok := github.ParseLookup(name)
if ok {
repo = repo_
ref = ref_
}
return
}

// the user may specific the harness plugin alias instead
// of the git repository. We are able to lookup the plugin
// by alias to find the corresponding repository and commit.
if repo == "" && kind != "bitrise" {
if repo == "" && kind == "harness" {
repo_, sha_, ok := harness.ParseLookup(name)
if ok {
repo = repo_
Expand All @@ -67,7 +66,7 @@ func main() {
// the user may specific the bitrise plugin alias instead
// of the git repository. We are able to lookup the plugin
// by alias to find the corresponding repository and commit.
if repo == "" && kind != "harness" {
if repo == "" && kind == "bitrise" {
repo_, sha_, ok := bitrise.ParseLookup(name)
if ok {
repo = repo_
Expand Down Expand Up @@ -132,11 +131,27 @@ func main() {
Environ: bitrise.Environ(
os.Environ(),
),
Outputfile: outputfile,
}
if err := execer.Exec(ctx); err != nil {
log.Error("step failed", err)
os.Exit(1)
}

case github.Is(codedir) || kind == "action":
log.Info("detected github action action.yml")
execer := github.Execer{
Name: name,
TmpDir: codedir,
Stdout: os.Stdout,
Stderr: os.Stderr,
Environ: os.Environ(),
Outputfile: outputfile,
}
if err := execer.Exec(ctx); err != nil {
log.Error("action step failed", err)
os.Exit(1)
}
default:
log.Info("unknown plugin type")
os.Exit(1)
Expand Down
43 changes: 43 additions & 0 deletions plugin/bitrise/bitrise.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@
package bitrise

import (
"github.com/joho/godotenv"
"gopkg.in/yaml.v3"
"io/ioutil"
"os"
"path/filepath"
)

const (
envStoreFile = ".envstore.yml"
)

// Is returns true if the root path is a Bitrise
// plugin repository.
func Is(root string) bool {
Expand All @@ -19,3 +26,39 @@ func Is(root string) bool {
}
return false
}

func exists(base, file string) bool {
path := filepath.Join(base, file)
if _, err := os.Stat(path); err == nil {
return true
}
return false
}

func readEnvStore(root string) (*envStore, error) {
buf, err := ioutil.ReadFile(filepath.Join(root, envStoreFile))
if err != nil {
return nil, err
}

m := &envStore{}
err = yaml.Unmarshal(buf, m)
if err != nil {
return nil, err
}

return m, err
}

func saveOutputFromEnvStore(envs []map[string]string, outputfile string) error {
finalMap := make(map[string]string)
for _, env := range envs {
for k, v := range env {
finalMap[k] = v
}
}
if len(finalMap) > 0 {
return godotenv.Write(finalMap, outputfile)
}
return nil
}
6 changes: 6 additions & 0 deletions plugin/bitrise/envstore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package bitrise

// envStore ...
type envStore struct {
Envs []map[string]string `json:"envs" yaml:"envs"`
}
33 changes: 28 additions & 5 deletions plugin/bitrise/execer.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ import (

// Execer executes a bitrise plugin.
type Execer struct {
Source string // plugin source code directory
Workdir string // pipeline working directory (aka workspace)
Environ []string
Stdout io.Writer
Stderr io.Writer
Source string // plugin source code directory
Workdir string // pipeline working directory (aka workspace)
Environ []string
Stdout io.Writer
Stderr io.Writer
Outputfile string
}

// Exec executes a bitrise plugin.
Expand Down Expand Up @@ -72,6 +73,17 @@ func (e *Execer) Exec(ctx context.Context) error {
}
}

// create the .envstore.yml file if not present
if !exists(e.Source, envStoreFile) {
slog.FromContext(ctx).
Debug("envman init")
cmd := exec.Command("envman", "init")
cmd.Dir = e.Source
if err := cmd.Run(); err != nil {
slog.FromContext(ctx).Warn("Unable to create envstore file", err)
}
}

module := out.Toolkit.Go.Module
if module == "" {
module = out.Toolkit.Go.PackageName
Expand Down Expand Up @@ -136,5 +148,16 @@ func (e *Execer) Exec(ctx context.Context) error {
}
}

// save to outputfile if present
if len(e.Outputfile) > 0 {
if m, err := readEnvStore(e.Source); err == nil && len(m.Envs) > 0 {
if err = saveOutputFromEnvStore(m.Envs, e.Outputfile); err != nil {
slog.FromContext(ctx).Error("Unable to save output", err)
}
} else if err != nil {
slog.FromContext(ctx).Error("Unable to load envstore file", err)
}
}

return nil
}
1 change: 1 addition & 0 deletions plugin/bitrise/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ type spec struct {
PackageName string `yaml:"package_name"`
}
}
Outputs []map[string]interface{} `yaml:"outputs"`
}
29 changes: 17 additions & 12 deletions plugin/github/execer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,29 @@ import (

// Execer executes a github action.
type Execer struct {
Name string
Environ []string
Stdout io.Writer
Stderr io.Writer
Name string
Environ []string
TmpDir string
Outputfile string
Stdout io.Writer
Stderr io.Writer
}

// Exec executes a github action.
func (e *Execer) Exec(ctx context.Context) error {
envVars := environ.Map(e.Environ)
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
return err
outputVars := make([]string, 0)
// parse the github plugin yaml
if out, _ := parseFile(getYamlFilename(e.TmpDir)); out != nil && len(out.Outputs) > 0 {
for k := range out.Outputs {
outputVars = append(outputVars, k)
}
}

workflowFile := filepath.Join(tmpDir, "workflow.yml")
beforeStepEnvFile := filepath.Join(tmpDir, "before.env")
afterStepEnvFile := filepath.Join(tmpDir, "after.env")
if err := createWorkflowFile(e.Name, envVars, workflowFile, beforeStepEnvFile, afterStepEnvFile); err != nil {
workflowFile := filepath.Join(e.TmpDir, "workflow.yml")
beforeStepEnvFile := filepath.Join(e.TmpDir, "before.env")
afterStepEnvFile := filepath.Join(e.TmpDir, "after.env")
if err := createWorkflowFile(e.Name, envVars, workflowFile, beforeStepEnvFile, afterStepEnvFile, e.Outputfile, outputVars); err != nil {
return err
}

Expand All @@ -54,7 +59,7 @@ func (e *Execer) Exec(ctx context.Context) error {
}

if eventPayload, ok := envVars["PLUGIN_EVENT_PAYLOAD"]; ok {
eventPayloadFile := filepath.Join(tmpDir, "event.yml")
eventPayloadFile := filepath.Join(e.TmpDir, "event.yml")

if err := ioutil.WriteFile(eventPayloadFile, []byte(eventPayload), 0644); err != nil {
return errors.Wrap(err, "failed to write event payload to file")
Expand Down
35 changes: 35 additions & 0 deletions plugin/github/github.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 harness provides support for executing Github plugins.
package github

import (
"os"
"path/filepath"
)

// Is returns true if the root path is a Harness
// plugin repository.
func Is(root string) bool {
path := filepath.Join(root, "action.yml")
if _, err := os.Stat(path); err == nil {
return true
}
path = filepath.Join(root, "action.yaml")
if _, err := os.Stat(path); err == nil {
return true
}
return false
}

func getYamlFilename(root string) string {
if _, err := os.Stat(filepath.Join(root, "action.yml")); err == nil {
return filepath.Join(root, "action.yml")
}
if _, err := os.Stat(filepath.Join(root, "action.yaml")); err == nil {
return filepath.Join(root, "action.yaml")
}
return ""
}
23 changes: 23 additions & 0 deletions plugin/github/lookup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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 github

import (
"net/url"
"strings"
)

// ParseLookup parses the step string and returns the
// associated repository and ref.
func ParseLookup(s string) (repo string, ref string, ok bool) {
if !strings.HasPrefix(s, "https://github.com") {
s, _ = url.JoinPath("https://github.com", s)
}

if parts := strings.SplitN(s, "@", 2); len(parts) == 2 {
return parts[0], parts[1], true
}
return s, "", true
}
30 changes: 30 additions & 0 deletions plugin/github/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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 github

import (
"io/ioutil"

"gopkg.in/yaml.v2"
)

// helper function to parse the bitrise plugin yaml.
func parse(b []byte) (*spec, error) {
out := new(spec)
err := yaml.Unmarshal(b, out)
if err != nil {
return nil, err
}
return out, nil
}

// helper function to parse the bitrise plugin yaml file.
func parseFile(s string) (*spec, error) {
raw, err := ioutil.ReadFile(s)
if err != nil {
return nil, err
}
return parse(raw)
}
10 changes: 10 additions & 0 deletions plugin/github/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// 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 github

// spec defines the bitrise plugin.
type spec struct {
Outputs map[string]interface{} `yaml:"outputs"`
}
Loading

0 comments on commit 1c16468

Please sign in to comment.