Skip to content

Commit

Permalink
150 add ci environment implementation in go (#151)
Browse files Browse the repository at this point in the history
* initial go implementation is green - now refactor

* add ci-environment implementation in Go (#150)

* add ci-environment implementation in Go (#150)

* comment staticcheck (#150)

* comment code coverage for now, check on using that with multiple languages (#150)

* address file name capitalization caught by go vet (#150)

* the environment variables already present while running in github actions had to be accounted for in the tests (#150)

* changes per code review (#150)
  • Loading branch information
dumpsterfireproject authored Jul 21, 2022
1 parent d78cb88 commit d0a4a15
Show file tree
Hide file tree
Showing 13 changed files with 750 additions and 0 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/release-go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Release Go

on:
push:
branches: [release/*]

jobs:
read-version:
name: Read version to release
runs-on: ubuntu-latest
outputs:
version: ${{ steps.versions.outputs.changelog-latest-version }}
steps:
- uses: actions/checkout@v3
- uses: cucumber/[email protected]
id: versions

publish-go:
name: Create go/v* tag
runs-on: ubuntu-latest
needs: read-version
steps:
- uses: actions/checkout@v3
- name: Create git tag
run: |
git tag "go/v${{ needs.read-version.outputs.version }}"
git push --tags
32 changes: 32 additions & 0 deletions .github/workflows/test-go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: test-go
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_call:

jobs:
test:
strategy:
matrix:
go-version: [ 1.17.x ]
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: lint
working-directory: go
run: gofmt -w .
- name: Run go vet
working-directory: go
run: go vet ./...
- name: Run go test
working-directory: go
run: make test
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
150 changes: 150 additions & 0 deletions go/CiEnvironments.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
[
{
"name": "Azure Pipelines",
"url": "${BUILD_BUILDURI}",
"buildNumber": "${BUILD_BUILDNUMBER}",
"git": {
"remote": "${BUILD_REPOSITORY_URI}",
"revision": "${BUILD_SOURCEVERSION}",
"branch": "${BUILD_SOURCEBRANCH/refs\/heads\/(.*)/\\1}",
"tag": "${BUILD_SOURCEBRANCH/refs\/tags\/(.*)/\\1}"
}
},
{
"name": "Bamboo",
"url": "${bamboo_buildResultsUrl}",
"buildNumber": "${bamboo_buildNumber}",
"git": {
"remote": "${bamboo_planRepository_repositoryUrl}",
"revision": "${bamboo_planRepository_revision}",
"branch": "${bamboo_planRepository_branch}"
}
},
{
"name": "Buddy",
"url": "${BUDDY_EXECUTION_URL}",
"buildNumber": "${BUDDY_EXECUTION_ID}",
"git": {
"remote": "${BUDDY_SCM_URL}",
"revision": "${BUDDY_EXECUTION_REVISION}",
"branch": "${BUDDY_EXECUTION_BRANCH}",
"tag": "${BUDDY_EXECUTION_TAG}"
}
},
{
"name": "Bitrise",
"url": "${BITRISE_BUILD_URL}",
"buildNumber": "${BITRISE_BUILD_NUMBER}",
"git": {
"remote": "${GIT_REPOSITORY_URL}",
"revision": "${BITRISE_GIT_COMMIT}",
"branch": "${BITRISE_GIT_BRANCH}",
"tag": "${BITRISE_GIT_TAG}"
}
},
{
"name": "CircleCI",
"url": "${CIRCLE_BUILD_URL}",
"buildNumber": "${CIRCLE_BUILD_NUM}",
"git": {
"remote": "${CIRCLE_REPOSITORY_URL}",
"revision": "${CIRCLE_SHA1}",
"branch": "${CIRCLE_BRANCH}",
"tag": "${CIRCLE_TAG}"
}
},
{
"name": "CodeFresh",
"url": "${CF_BUILD_URL}",
"buildNumber": "${CF_BUILD_ID}",
"git": {
"remote": "${CF_COMMIT_URL/(.*)\\/commit.+$/\\1}.git",
"revision": "${CF_REVISION}",
"branch": "${CF_BRANCH}"
}
},
{
"name": "CodeShip",
"url": "${CI_BUILD_URL}",
"buildNumber": "${CI_BUILD_NUMBER}",
"git": {
"remote": "${CI_PULL_REQUEST/(.*)\\/pull\\/\\d+/\\1.git}",
"revision": "${CI_COMMIT_ID}",
"branch": "${CI_BRANCH}"
}
},
{
"name": "GitHub Actions",
"url": "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}",
"buildNumber": "${GITHUB_RUN_ID}",
"git": {
"remote": "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git",
"revision": "${GITHUB_SHA}",
"branch": "${GITHUB_HEAD_REF}",
"tag": "${GITHUB_REF/refs\/tags\/(.*)/\\1}"
}
},
{
"name": "GitLab",
"url": "${CI_JOB_URL}",
"buildNumber": "${CI_JOB_ID}",
"git": {
"remote": "${CI_REPOSITORY_URL}",
"revision": "${CI_COMMIT_SHA}",
"branch": "${CI_COMMIT_BRANCH}",
"tag": "${CI_COMMIT_TAG}"
}
},
{
"name": "GoCD",
"url": "${GO_SERVER_URL}/pipelines/${GO_PIPELINE_NAME}/${GO_PIPELINE_COUNTER}/${GO_STAGE_NAME}/${GO_STAGE_COUNTER}",
"buildNumber": "${GO_PIPELINE_NAME}/${GO_PIPELINE_COUNTER}/${GO_STAGE_NAME}/${GO_STAGE_COUNTER}",
"git": {
"remote": "${GO_SCM_*_PR_URL/(.*)\\/pull\\/\\d+/\\1.git}",
"revision": "${GO_REVISION}",
"branch": "${GO_SCM_*_PR_BRANCH/.*:(.*)/\\1}"
}
},
{
"name": "Jenkins",
"url": "${BUILD_URL}",
"buildNumber": "${BUILD_NUMBER}",
"git": {
"remote": "${GIT_URL}",
"revision": "${GIT_COMMIT}",
"branch": "${GIT_LOCAL_BRANCH}"
}
},
{
"name": "Semaphore",
"url": "${SEMAPHORE_ORGANIZATION_URL}/jobs/${SEMAPHORE_JOB_ID}",
"buildNumber": "${SEMAPHORE_JOB_ID}",
"git": {
"remote": "${SEMAPHORE_GIT_URL}",
"revision": "${SEMAPHORE_GIT_SHA}",
"branch": "${SEMAPHORE_GIT_BRANCH}",
"tag": "${SEMAPHORE_GIT_TAG_NAME}"
}
},
{
"name": "Travis CI",
"url": "${TRAVIS_BUILD_WEB_URL}",
"buildNumber": "${TRAVIS_JOB_NUMBER}",
"git": {
"remote": "https://github.com/${TRAVIS_REPO_SLUG}.git",
"revision": "${TRAVIS_COMMIT}",
"branch": "${TRAVIS_BRANCH}",
"tag": "${TRAVIS_TAG}"
}
},
{
"name": "Wercker",
"url": "${WERCKER_RUN_URL}",
"buildNumber": "${WERCKER_RUN_URL/.*\\/([^\\/]+)$/\\1}",
"git": {
"remote": "https://${WERCKER_GIT_DOMAIN}/${WERCKER_GIT_OWNER}/${WERCKER_GIT_REPOSITORY}.git",
"revision": "${WERCKER_GIT_COMMIT}",
"branch": "${WERCKER_GIT_BRANCH}"
}
}
]
16 changes: 16 additions & 0 deletions go/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FOUND_GO_VERSION := $(shell go version)
EXPECTED_GO_VERSION = 1.17

copy-template:
cp ../CiEnvironments.json CiEnvironments.json

check-go-version:
@$(if $(findstring ${EXPECTED_GO_VERSION}, ${FOUND_GO_VERSION}),(exit 0),(echo Wrong go version! Please install ${EXPECTED_GO_VERSION}; exit 1))

test: copy-template check-go-version
@echo "running all tests"
@go install ./...
@go fmt ./...
@go run honnef.co/go/tools/cmd/[email protected] github.com/cucumber/ci-environment/go
go vet ./...
go test ./...
31 changes: 31 additions & 0 deletions go/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# CI-Environment: Go

This directory contains the Go implementation of the ci-environment implementation.

## Template Locations
To embed the templates for the ci-environment go module, we'll use go:embed to include CIEnvironments.json. Since go:embed does not support paths which include `..`, we can't just use a path the the CIEnvironments.json file in the root of the git repository. So a Makefile is included in this go module which will handle copying the most recent template from the root of the git repository to the go directory. The CiEnvironments.json file contains the templates to map available environment variables to values in templates for the variety of supported CI enviroments.

## Template Expressions

### Simple Environment Variables
Many are simple replacements of environment variables. E.g, for Azure DevOps, the URI is simply the value of the `${BUILD_BUILDURI}` environment variable.

### Multiple Environment Variables and/or Constants
For others, it is a combination of multiple enviroment variables and constants. E.g., for Github Actions, the URI is the combination of `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`.

### Expression Syntax
Still for others, there is some additional processing of values that can be found in environment variables. In these cases, the value in the template consists of three parts: the environment variable to be used, the regex expression (including capture groups) to to used to derive the final value, and the template to be applied using the value(s) from those
capture groups.

E.g., the Git remote template for CodeShip has a value of `${CI_PULL_REQUEST/(.*)\\/pull\\/\\d+/\\1.git}`. The sections of this expression are delineated by unescaped slashes. So the three parts are the `CI_PULL_REQUEST` environment variable, the regex of `(.*)\\/pull\\/\\d+`, and the template of `\\1.git`.

So in this example, the URL in the CI_PULL_REQUEST environment variable will have everything prior to `pull/\d+` in a capture group, then the first (and only in this example) capture group is prepended to the literal `.git`. So if the CI_PULL_REQUEST envrionment variable were `https://github.com/owner/repo/pull/42`, then the value of `https://github.com/owner/repo` would be part of that first capture group, and then the resulting value will be `https://github.com/owner/repo.git`.

Or as explained in ARCHITECTURE.md:
>The expression syntax for environment variables can use the form `${variable/pattern/replacement}`, similar to [bash parameter substitution](https://tldp.org/LDP/abs/html/parameter-substitution.html), but inspired from [sed's s command](https://www.gnu.org/software/sed/manual/html_node/The-_0022s_0022-Command.html) which provides support for capture group back-references in the replacement.
### Wildcards in Environment Variable Names
In some expressions, an environment variable name can contain a wildcard, e.g., GoCD. The git branch is derived using `GO_SCM_*_PR_BRANCH`. In this situation, the environment variable name contains the branch name (e.g., GO_SCM_MY_MATERIAL_PR_BRANCH where the branch name is MY_MATERIAL) and is thus not a constant. So the wildcard is used as a placeholder and when evaluating the expression, we must loop through all the environment variables until we find the one that matches the pattern.

## Why Parse the Template Expressions?
In other implementations, such as the javascript implementation for ci-environments, a regex can be used when evaluating the variable/pattern/replacement expressions. An example regex is `'\\${(.*?)(?:(?<!\\\\)/(.*)/(.*))?}'`. However, for Go, the [Google/re2](https://github.com/google/re2/wiki/Syntax) syntax does not support `after text not matching` expressions in the form of `(?<!re)`. So a quick and simple parser was put together to evaluate these expressions in lieu of using regex.
51 changes: 51 additions & 0 deletions go/ci_environment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cienvironment

import (
"net/url"
)

type Git struct {
Remote string `json:"remote"`
Revision string `json:"revision"`
Branch string `json:"branch"`
Tag string `json:"tag"`
}

type CiEnvironment struct {
Name string `json:"name"`
URL string `json:"url"`
BuildNumber string `json:"buildNumber"`
Git *Git `json:"git"`
}

// IsPresent returns true is the CiEnvironment has a URL that has been built using detected environment variables.
func (c *CiEnvironment) IsPresent() bool {
return len(c.URL) > 0
}

// 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
}
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
}
u, err := url.Parse(c.Git.Remote)
if err != nil {
return c
}
u.User = nil
c.Git.Remote = u.String()
return c
}
36 changes: 36 additions & 0 deletions go/ci_environment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package cienvironment_test

import (
"testing"

cienvironment "github.com/cucumber/ci-environment/go"
"github.com/stretchr/testify/assert"
)

func TestRemoveUserInfo(t *testing.T) {
testCases := []struct {
input *cienvironment.CiEnvironment
want *cienvironment.Git
}{
{
&cienvironment.CiEnvironment{},
nil,
},
{
&cienvironment.CiEnvironment{Git: &cienvironment.Git{Revision: "2a2f73c6", Remote: "https://cihost.com"}},
&cienvironment.Git{Revision: "2a2f73c6", Remote: "https://cihost.com"},
},
{
&cienvironment.CiEnvironment{Git: &cienvironment.Git{Revision: "2a2f73c6", Remote: "https://user:[email protected]"}},
&cienvironment.Git{Revision: "2a2f73c6", Remote: "https://cihost.com"},
},
{
&cienvironment.CiEnvironment{Git: &cienvironment.Git{Revision: "2a2f73c6", Remote: "not_a_valid_url"}},
&cienvironment.Git{Revision: "2a2f73c6", Remote: "not_a_valid_url"},
},
}
for _, tc := range testCases {
got := tc.input.RemoveUserInfo().Git
assert.Equal(t, tc.want, got, tc.input.Git)
}
}
Loading

0 comments on commit d0a4a15

Please sign in to comment.