From 97951cab6cc3615a9e9ac8d7a732df233383bf67 Mon Sep 17 00:00:00 2001 From: Brian Hnat Date: Tue, 19 Jul 2022 21:28:52 -0400 Subject: [PATCH] add ci-environment implementation in Go (#150) --- CHANGELOG.md | 2 ++ README.md | 21 +++++++++++++++++ go/ci_environment.go | 15 +++--------- go/ci_environment_test.go | 44 ----------------------------------- go/detect_environment.go | 22 ++++++++++++++---- go/detect_environment_test.go | 4 ++-- 6 files changed, 46 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd8e0510..5277e71b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index f84380ce..c731bc11 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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) diff --git a/go/ci_environment.go b/go/ci_environment.go index 414daff2..90b25d9f 100644 --- a/go/ci_environment.go +++ b/go/ci_environment.go @@ -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 diff --git a/go/ci_environment_test.go b/go/ci_environment_test.go index c5fd4e21..4ac73184 100644 --- a/go/ci_environment_test.go +++ b/go/ci_environment_test.go @@ -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 diff --git a/go/detect_environment.go b/go/detect_environment.go index 735b5c83..d13f9081 100644 --- a/go/detect_environment.go +++ b/go/detect_environment.go @@ -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) @@ -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) @@ -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) @@ -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) @@ -65,6 +74,7 @@ func evalutate(expression string) (string, error) { return result, nil } +// any literal or variable expression type configValue interface { Value() string } @@ -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() @@ -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 = "" } @@ -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)) @@ -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 { @@ -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 { diff --git a/go/detect_environment_test.go b/go/detect_environment_test.go index 47b9da16..1a3fafc7 100644 --- a/go/detect_environment_test.go +++ b/go/detect_environment_test.go @@ -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)