Skip to content

Commit

Permalink
add ci-environment implementation in Go (#150)
Browse files Browse the repository at this point in the history
  • Loading branch information
dumpsterfireproject committed Jul 20, 2022
1 parent 3604a41 commit 97951ca
Show file tree
Hide file tree
Showing 6 changed files with 46 additions and 62 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- [Go] added ci-environment implementation in Go

## [9.0.4] - 2022-03-06
### Fixed
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![test-java](https://github.com/cucumber/ci-environment/actions/workflows/test-java.yml/badge.svg)](https://github.com/cucumber/ci-environment/actions/workflows/test-java.yml)
[![test-javascript](https://github.com/cucumber/ci-environment/actions/workflows/test-javascript.yml/badge.svg)](https://github.com/cucumber/ci-environment/actions/workflows/test-javascript.yml)
[![test-ruby](https://github.com/cucumber/ci-environment/actions/workflows/test-ruby.yml/badge.svg)](https://github.com/cucumber/ci-environment/actions/workflows/test-ruby.yml)
[![test-go](https://github.com/cucumber/ci-environment/actions/workflows/test-go.yml/badge.svg)](https://github.com/cucumber/ci-environment/actions/workflows/test-go.yml)

This library detects the CI environment based on environment variables defined
by CI servers.
Expand Down Expand Up @@ -76,6 +77,26 @@ ci_environment = Cucumber::CiEnvironment.detect_ci_environment(ENV)
p ci_environment
```

### Go

```shell
go get github.com/cucumber/ci-environment/go@latest
```

```Go
import (
"fmt"
cienvironment "github.com/cucumber/ci-environment/go"
)

func main() {
ci := cienvironment.DetectCIEnvironment()
if ci == nil {
fmt.Println("No CI environment detected")
}
}
```

## Supported CI servers

* [Azure Pipelines](https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?tabs=yaml&view=azure-devops#build-variables)
Expand Down
15 changes: 3 additions & 12 deletions go/ci_environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,24 @@ type CiEnvironment struct {
}

// IsPresent returns true is the CiEnvironment has a URL that has been built using detected environment variables.
// Most CI Environments have a URL that is a single environment variable, e.g., ${BUILD_BUILDURI}.
// A few are a combination of literals and multiple environment variables, e.g., ${SEMAPHORE_ORGANIZATION_URL}/jobs/${SEMAPHORE_JOB_ID}.
func (c *CiEnvironment) IsPresent() bool {
// switch c.Name {
// case "GitHub Actions":
// return c.URL != "//actions/runs/"
// case "GoCD":
// return c.URL != "/pipelines////"
// case "Semaphore":
// return c.URL != "/jobs/"
// }
return len(c.URL) > 0
}

// SanitizeGit removes any non-empty Git fields, when the Git.Revision is empty.
// SanitizeGit removes any non-empty Git fields, when the Git.Revision or Git.Remote is empty.
// Standardizes that Git is nil when no Git repository is detected.
// Remove any user info from the git remote value.
func (c *CiEnvironment) SanitizeGit() *CiEnvironment {
if c.Git == nil {
return c
}
// fmt.Printf("---> remote (%s) revision (%s) branch (%s)\n", c.Git.Remote, c.Git.Revision, c.Git.Branch)
if len(c.Git.Remote) == 0 || len(c.Git.Revision) == 0 {
c.Git = nil
}
return c.RemoveUserInfo()
}

// RemoveUserInfo removes the user info, especially password, from the Git remote URL
func (c *CiEnvironment) RemoveUserInfo() *CiEnvironment {
if c.Git == nil {
return c
Expand Down
44 changes: 0 additions & 44 deletions go/ci_environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,6 @@ import (
"github.com/stretchr/testify/assert"
)

// func TestIsPresent(t *testing.T) {
// testCases := []struct {
// ciEnvironment *cienvironment.CiEnvironment
// want bool
// }{
// {&cienvironment.CiEnvironment{Name: "Azure Pipelines", URL: "https://cihost.com/path/to/the/build"}, true},
// {&cienvironment.CiEnvironment{Name: "Azure Pipelines", URL: ""}, false},
// {&cienvironment.CiEnvironment{Name: "GitHub Actions", URL: "https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429"}, true},
// {&cienvironment.CiEnvironment{Name: "GitHub Actions", URL: "//actions/runs/"}, false},
// {&cienvironment.CiEnvironment{Name: "GoCD", URL: "https://cihost.com/pipelines/pname/154666428/sname/154666429"}, true},
// {&cienvironment.CiEnvironment{Name: "GoCD", URL: "/pipelines////"}, false},
// {&cienvironment.CiEnvironment{Name: "Semaphore", URL: "https://cihost.com/jobs/154666429"}, true},
// {&cienvironment.CiEnvironment{Name: "Semaphore", URL: "/jobs/"}, false},
// }
// for _, tc := range testCases {
// got := tc.ciEnvironment.IsPresent()
// assert.Equal(t, tc.want, got, tc.ciEnvironment.URL)
// }
// }

// func TestSanitizeGit(t *testing.T) {
// testCases := []struct {
// input *cienvironment.CiEnvironment
// want *cienvironment.Git
// }{
// {
// &cienvironment.CiEnvironment{},
// nil,
// },
// {
// &cienvironment.CiEnvironment{Git: &cienvironment.Git{}},
// nil,
// },
// {
// &cienvironment.CiEnvironment{Git: &cienvironment.Git{Revision: "2a2f73c6"}},
// &cienvironment.Git{Revision: "2a2f73c6"},
// },
// }
// for _, tc := range testCases {
// got := tc.input.SanitizeGit()
// assert.Equal(t, tc.want, got.Git, tc.input.Git)
// }
// }

func TestRemoveUserInfo(t *testing.T) {
testCases := []struct {
input *cienvironment.CiEnvironment
Expand Down
22 changes: 18 additions & 4 deletions go/detect_environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ var ciEnvironmentTemplates string

func DetectCIEnvironment() *CiEnvironment {
ciEnvironments := []*CiEnvironment{}
re := regexp.MustCompile(`([^\\])\\/`) // TODO: Explain Azure

// The Azure template currently only has single back-slashes to escape the slashes in the variable/pattern/replacement
// patterns "${BUILD_SOURCEBRANCH/refs\/heads\/(.*)/\\1}" where others are like "${CI_PULL_REQUEST/(.*)\\/pull\\/\\d+/\\1.git}".
// To make these all consistant prior to parsing, we'll replace those with double back-slashes prior to processing.
re := regexp.MustCompile(`([^\\])\\/`)
template := re.ReplaceAllString(ciEnvironmentTemplates, `${1}\\/`)
err := json.Unmarshal([]byte(template), &ciEnvironments)

if err != nil {
l := log.New(os.Stderr, "", 0)
l.Printf("error parsing ci templates %s", err)
Expand All @@ -28,6 +33,7 @@ func DetectCIEnvironment() *CiEnvironment {
var environment *CiEnvironment = nil
for _, ciEnvironment := range ciEnvironments {

// evaluate the expressions for each part of the ci environment configuration
ciEnvironment.URL, _ = evalutate(ciEnvironment.URL)
ciEnvironment.BuildNumber, _ = evalutate(ciEnvironment.BuildNumber)
ciEnvironment.Git.Branch, _ = evalutate(ciEnvironment.Git.Branch)
Expand All @@ -46,6 +52,7 @@ func DetectCIEnvironment() *CiEnvironment {
return environment.SanitizeGit()
}

// evaluate each token parsed from the expression and create a value by concatenating all evaluated tokens.
func evalutate(expression string) (string, error) {
result := ""
sc := NewScanner(expression)
Expand All @@ -55,6 +62,8 @@ func evalutate(expression string) (string, error) {
case *variableExpression:
s, err := t.Evaluate()
if err != nil {
l := log.New(os.Stderr, "", 0)
l.Printf("error evalutating %s: %s", expression, err)
return result, err
}
result = fmt.Sprintf("%s%s", result, s)
Expand All @@ -65,6 +74,7 @@ func evalutate(expression string) (string, error) {
return result, nil
}

// any literal or variable expression
type configValue interface {
Value() string
}
Expand All @@ -82,9 +92,7 @@ func (v *variableExpression) Evaluate() (string, error) {
var buf bytes.Buffer
prev := eof

// expression := strings.ReplaceAll(v.value, `\\`, `\`)

for _, ch := range v.value { //expression {
for _, ch := range v.value {
if ch == '/' && prev != '\\' {
tokens = append(tokens, buf.String())
buf.Reset()
Expand All @@ -110,6 +118,7 @@ func (v *variableExpression) Evaluate() (string, error) {
}
matches = re.FindAllStringSubmatch(value, -1)
case 2:
// if no matches exist, then this expression has no value
if len(matches) == 0 {
value = ""
}
Expand All @@ -128,6 +137,9 @@ func (v *variableExpression) Evaluate() (string, error) {
return value, nil
}

// getEnv return the environment variable if no wildcard is included in the variable name. If the variable name
// does contain a wildcard, then convert to a valid regex and check all envrionemtn variables until we find one that
// matches, if a match does exist.
func getEnv(name string) string {
if strings.Contains(name, "*") {
re := regexp.MustCompile(strings.Replace(name, "*", ".*", -1))
Expand Down Expand Up @@ -162,6 +174,7 @@ func NewScanner(expression string) Scanner {

var eof = rune(0)

// ReadTokens returns an array of all literal and variable tokens found in the expression being scanned
func (s Scanner) ReadTokens() []configValue {
tokens := []configValue{}
for {
Expand All @@ -174,6 +187,7 @@ func (s Scanner) ReadTokens() []configValue {
return tokens
}

// Next returns the next token from the scanner
func (s Scanner) Next() configValue {
ch := s.read()
if ch == eof {
Expand Down
4 changes: 2 additions & 2 deletions go/detect_environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ type testCase struct {
func TestDetectCIEnvironment(t *testing.T) {
testCases := loadTestData()
for _, tc := range testCases {
t.Run(tc.fileName, testFoo(tc.envVars, tc.want))
t.Run(tc.fileName, testDetectCIEnvironment(tc.envVars, tc.want))
}
}

func testFoo(envVars map[string]string, want *cienvironment.CiEnvironment) func(*testing.T) {
func testDetectCIEnvironment(envVars map[string]string, want *cienvironment.CiEnvironment) func(*testing.T) {
return func(t *testing.T) {
for k, v := range envVars {
t.Setenv(k, v)
Expand Down

0 comments on commit 97951ca

Please sign in to comment.