1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +02:00

feat(orchestrator): add implementation for GitHub (#4525)

* add comments with examples to methods

* a bit refactoring and cleanup

* actionsURL

* GetBuildStatus

* GetBuildID, GetChangeSet, GetPipelineStartTime

* GetStageName and GetBuildReason

* refactor fetching jobs

* GetJobName and GetJobURL

* chnage GetBuildURL

* refactor actionsURL

* fix guessCurrentJob bug

* unit tests for all

* refactor GetLog

* refactor and fix tests

* change GetBuildURL to use env vars

* fix issues

* leftover

* add comment

* fix according to review comments

---------

Co-authored-by: Gulom Alimov <gulomjon.alimov@sap.com>
Co-authored-by: Jordi van Liempt <35920075+jliempt@users.noreply.github.com>
This commit is contained in:
Googlom 2023-08-29 12:32:35 +05:00 committed by GitHub
parent b7663466f3
commit e805beda70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 531 additions and 296 deletions

View File

@ -13,20 +13,17 @@ import (
type AzureDevOpsConfigProvider struct {
client piperHttp.Client
options piperHttp.ClientOptions
apiInformation map[string]interface{}
}
// InitOrchestratorProvider initializes http client for AzureDevopsConfigProvider
func (a *AzureDevOpsConfigProvider) InitOrchestratorProvider(settings *OrchestratorSettings) {
a.client = piperHttp.Client{}
a.options = piperHttp.ClientOptions{
a.client.SetOptions(piperHttp.ClientOptions{
Username: "",
Password: settings.AzureToken,
MaxRetries: 3,
TransportTimeout: time.Second * 10,
}
a.client.SetOptions(a.options)
})
log.Entry().Debug("Successfully initialized Azure config provider")
}
@ -102,12 +99,12 @@ func (a *AzureDevOpsConfigProvider) GetBuildStatus() string {
// cases to align with Jenkins: SUCCESS, FAILURE, NOT_BUILD, ABORTED
switch buildStatus := getEnv("AGENT_JOBSTATUS", "FAILURE"); buildStatus {
case "Succeeded":
return "SUCCESS"
return BuildStatusSuccess
case "Canceled":
return "ABORTED"
return BuildStatusAborted
default:
// Failed, SucceededWithIssues
return "FAILURE"
return BuildStatusFailure
}
}

View File

@ -2,7 +2,6 @@ package orchestrator
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
@ -14,53 +13,51 @@ import (
"github.com/SAP/jenkins-library/pkg/log"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
)
var httpHeader = http.Header{
"Accept": {"application/vnd.github+json"},
type GitHubActionsConfigProvider struct {
client piperHttp.Client
runData run
jobs []job
jobsFetched bool
currentJob job
}
type GitHubActionsConfigProvider struct {
client piperHttp.Client
type run struct {
fetched bool
Status string `json:"status"`
StartedAt time.Time `json:"run_started_at"`
}
type job struct {
ID int `json:"id"`
ID int `json:"id"`
Name string `json:"name"`
HtmlURL string `json:"html_url"`
}
type stagesID struct {
Jobs []job `json:"jobs"`
}
type logs struct {
type fullLog struct {
sync.Mutex
b [][]byte
}
var httpHeaders = http.Header{
"Accept": {"application/vnd.github+json"},
"X-GitHub-Api-Version": {"2022-11-28"},
}
// InitOrchestratorProvider initializes http client for GitHubActionsDevopsConfigProvider
func (g *GitHubActionsConfigProvider) InitOrchestratorProvider(settings *OrchestratorSettings) {
g.client = piperHttp.Client{}
g.client.SetOptions(piperHttp.ClientOptions{
Password: settings.GitHubToken,
Token: "Bearer " + settings.GitHubToken,
MaxRetries: 3,
TransportTimeout: time.Second * 10,
})
log.Entry().Debug("Successfully initialized GitHubActions config provider")
}
func getActionsURL() string {
ghURL := getEnv("GITHUB_URL", "")
switch ghURL {
case "https://github.com/":
ghURL = "https://api.github.com"
default:
ghURL += "api/v3"
}
return fmt.Sprintf("%s/repos/%s/actions", ghURL, getEnv("GITHUB_REPOSITORY", ""))
}
func (g *GitHubActionsConfigProvider) OrchestratorVersion() string {
log.Entry().Debugf("OrchestratorVersion() for GitHub Actions is not applicable.")
return "n/a"
}
@ -68,109 +65,141 @@ func (g *GitHubActionsConfigProvider) OrchestratorType() string {
return "GitHubActions"
}
// GetBuildStatus returns current run status
func (g *GitHubActionsConfigProvider) GetBuildStatus() string {
log.Entry().Infof("GetBuildStatus() for GitHub Actions not yet implemented.")
return "FAILURE"
g.fetchRunData()
switch g.runData.Status {
case "success":
return BuildStatusSuccess
case "cancelled":
return BuildStatusAborted
case "in_progress":
return BuildStatusInProgress
default:
return BuildStatusFailure
}
}
// GetLog returns the whole logfile for the current pipeline run
func (g *GitHubActionsConfigProvider) GetLog() ([]byte, error) {
ids, err := g.getStageIds()
if err != nil {
if err := g.fetchJobs(); err != nil {
return nil, err
}
// Ignore the last stage (job) as it is not possible in GitHub to fetch logs for a running job.
jobs := g.jobs[:len(g.jobs)-1]
logs := logs{
b: make([][]byte, len(ids)),
}
ctx := context.Background()
sem := semaphore.NewWeighted(10)
fullLogs := fullLog{b: make([][]byte, len(jobs))}
wg := errgroup.Group{}
for i := range ids {
wg.SetLimit(10)
for i := range jobs {
i := i // https://golang.org/doc/faq#closures_and_goroutines
if err := sem.Acquire(ctx, 1); err != nil {
return nil, fmt.Errorf("failed to acquire semaphore: %w", err)
}
wg.Go(func() error {
defer sem.Release(1)
resp, err := g.client.GetRequest(fmt.Sprintf("%s/jobs/%d/logs", getActionsURL(), ids[i]), httpHeader, nil)
resp, err := g.client.GetRequest(fmt.Sprintf("%s/jobs/%d/logs", actionsURL(), jobs[i].ID), httpHeaders, nil)
if err != nil {
return fmt.Errorf("failed to get API data: %w", err)
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
defer resp.Body.Close()
logs.Lock()
defer logs.Unlock()
logs.b[i] = b
fullLogs.Lock()
fullLogs.b[i] = b
fullLogs.Unlock()
return nil
})
}
if err = wg.Wait(); err != nil {
if err := wg.Wait(); err != nil {
return nil, fmt.Errorf("failed to get logs: %w", err)
}
return bytes.Join(logs.b, []byte("")), nil
return bytes.Join(fullLogs.b, []byte("")), nil
}
// GetBuildID returns current run ID
func (g *GitHubActionsConfigProvider) GetBuildID() string {
log.Entry().Infof("GetBuildID() for GitHub Actions not yet implemented.")
return "n/a"
return getEnv("GITHUB_RUN_ID", "n/a")
}
func (g *GitHubActionsConfigProvider) GetChangeSet() []ChangeSet {
log.Entry().Warn("GetChangeSet for GitHubActions not yet implemented")
log.Entry().Debug("GetChangeSet for GitHubActions not implemented")
return []ChangeSet{}
}
// GetPipelineStartTime returns the pipeline start time in UTC
func (g *GitHubActionsConfigProvider) GetPipelineStartTime() time.Time {
log.Entry().Infof("GetPipelineStartTime() for GitHub Actions not yet implemented.")
return time.Time{}.UTC()
g.fetchRunData()
return g.runData.StartedAt.UTC()
}
// GetStageName returns the human-readable name given to a stage.
func (g *GitHubActionsConfigProvider) GetStageName() string {
return "GITHUB_WORKFLOW" // TODO: is there something like is "stage" in GH Actions?
return getEnv("GITHUB_JOB", "unknown")
}
// GetBuildReason returns the reason of workflow trigger.
// BuildReasons are unified with AzureDevOps build reasons, see
// https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables-devops-services
func (g *GitHubActionsConfigProvider) GetBuildReason() string {
log.Entry().Infof("GetBuildReason() for GitHub Actions not yet implemented.")
return "n/a"
switch getEnv("GITHUB_EVENT_NAME", "") {
case "workflow_dispatch":
return BuildReasonManual
case "schedule":
return BuildReasonSchedule
case "pull_request":
return BuildReasonPullRequest
case "workflow_call":
return BuildReasonResourceTrigger
case "push":
return BuildReasonIndividualCI
default:
return BuildReasonUnknown
}
}
// GetBranch returns the source branch name, e.g. main
func (g *GitHubActionsConfigProvider) GetBranch() string {
return strings.TrimPrefix(getEnv("GITHUB_REF", "n/a"), "refs/heads/")
return getEnv("GITHUB_REF_NAME", "n/a")
}
// GetReference return the git reference. For example, refs/heads/your_branch_name
func (g *GitHubActionsConfigProvider) GetReference() string {
return getEnv("GITHUB_REF", "n/a")
}
// GetBuildURL returns the builds URL. For example, https://github.com/SAP/jenkins-library/actions/runs/5815297487
func (g *GitHubActionsConfigProvider) GetBuildURL() string {
return g.GetRepoURL() + "/actions/runs/" + getEnv("GITHUB_RUN_ID", "n/a")
return g.GetRepoURL() + "/actions/runs/" + g.GetBuildID()
}
// GetJobURL returns the current job HTML URL (not API URL).
// For example, https://github.com/SAP/jenkins-library/actions/runs/123456/jobs/7654321
func (g *GitHubActionsConfigProvider) GetJobURL() string {
log.Entry().Debugf("Not yet implemented.")
return g.GetRepoURL() + "/actions/runs/" + getEnv("GITHUB_RUN_ID", "n/a")
// We need to query the GitHub API here because the environment variable GITHUB_JOB returns
// the name of the job, not a numeric ID (which we need to form the URL)
g.guessCurrentJob()
return g.currentJob.HtmlURL
}
// GetJobName returns the current workflow name. For example, "Piper workflow"
func (g *GitHubActionsConfigProvider) GetJobName() string {
log.Entry().Debugf("GetJobName() for GitHubActions not yet implemented.")
return "n/a"
return getEnv("GITHUB_WORKFLOW", "unknown")
}
// GetCommit returns the commit SHA that triggered the workflow. For example, ffac537e6cbbf934b08745a378932722df287a53
func (g *GitHubActionsConfigProvider) GetCommit() string {
return getEnv("GITHUB_SHA", "n/a")
}
// GetRepoURL returns full url to repository. For example, https://github.com/SAP/jenkins-library
func (g *GitHubActionsConfigProvider) GetRepoURL() string {
return getEnv("GITHUB_SERVER_URL", "n/a") + "/" + getEnv("GITHUB_REPOSITORY", "n/a")
}
// GetPullRequestConfig returns pull request configuration
func (g *GitHubActionsConfigProvider) GetPullRequestConfig() PullRequestConfig {
// See https://docs.github.com/en/enterprise-server@3.6/actions/learn-github-actions/variables#default-environment-variables
githubRef := getEnv("GITHUB_REF", "n/a")
@ -182,6 +211,7 @@ func (g *GitHubActionsConfigProvider) GetPullRequestConfig() PullRequestConfig {
}
}
// IsPullRequest indicates whether the current build is triggered by a PR
func (g *GitHubActionsConfigProvider) IsPullRequest() bool {
return truthy("GITHUB_HEAD_REF")
}
@ -191,26 +221,82 @@ func isGitHubActions() bool {
return areIndicatingEnvVarsSet(envVars)
}
func (g *GitHubActionsConfigProvider) getStageIds() ([]int, error) {
resp, err := g.client.GetRequest(fmt.Sprintf("%s/runs/%s/jobs", getActionsURL(), getEnv("GITHUB_RUN_ID", "")), httpHeader, nil)
if err != nil {
return nil, fmt.Errorf("failed to get API data: %w", err)
}
var stagesID stagesID
err = piperHttp.ParseHTTPResponseBodyJSON(resp, &stagesID)
if err != nil {
return nil, fmt.Errorf("failed to parse JSON data: %w", err)
}
ids := make([]int, len(stagesID.Jobs))
for i, job := range stagesID.Jobs {
ids[i] = job.ID
}
if len(ids) == 0 {
return nil, fmt.Errorf("failed to get IDs")
}
// execution of the last stage hasn't finished yet - we can't get logs of the last stage
return ids[:len(stagesID.Jobs)-1], nil
// actionsURL returns URL to actions resource. For example,
// https://api.github.com/repos/SAP/jenkins-library/actions
func actionsURL() string {
return getEnv("GITHUB_API_URL", "") + "/repos/" + getEnv("GITHUB_REPOSITORY", "") + "/actions"
}
func (g *GitHubActionsConfigProvider) fetchRunData() {
if g.runData.fetched {
return
}
url := fmt.Sprintf("%s/runs/%s", actionsURL(), getEnv("GITHUB_RUN_ID", ""))
resp, err := g.client.GetRequest(url, httpHeaders, nil)
if err != nil || resp.StatusCode != 200 {
log.Entry().Errorf("failed to get API data: %s", err)
return
}
err = piperHttp.ParseHTTPResponseBodyJSON(resp, &g.runData)
if err != nil {
log.Entry().Errorf("failed to parse JSON data: %s", err)
return
}
g.runData.fetched = true
}
func (g *GitHubActionsConfigProvider) fetchJobs() error {
if g.jobsFetched {
return nil
}
url := fmt.Sprintf("%s/runs/%s/jobs", actionsURL(), g.GetBuildID())
resp, err := g.client.GetRequest(url, httpHeaders, nil)
if err != nil {
return fmt.Errorf("failed to get API data: %w", err)
}
var result struct {
Jobs []job `json:"jobs"`
}
err = piperHttp.ParseHTTPResponseBodyJSON(resp, &result)
if err != nil {
return fmt.Errorf("failed to parse JSON data: %w", err)
}
if len(result.Jobs) == 0 {
return fmt.Errorf("no jobs found in response")
}
g.jobs = result.Jobs
g.jobsFetched = true
return nil
}
func (g *GitHubActionsConfigProvider) guessCurrentJob() {
// check if the current job has already been guessed
if g.currentJob.ID != 0 {
return
}
// fetch jobs if they haven't been fetched yet
if err := g.fetchJobs(); err != nil {
log.Entry().Errorf("failed to fetch jobs: %s", err)
g.jobs = []job{}
return
}
targetJobName := getEnv("GITHUB_JOB", "unknown")
log.Entry().Debugf("looking for job '%s' in jobs list: %v", targetJobName, g.jobs)
for _, j := range g.jobs {
// j.Name may be something like "piper / Init / Init"
// but GITHUB_JOB env may contain only "Init"
if strings.HasSuffix(j.Name, targetJobName) {
log.Entry().Debugf("current job id: %d", j.ID)
g.currentJob = j
return
}
}
}

View File

@ -5,9 +5,9 @@ package orchestrator
import (
"fmt"
"math/rand"
"net/http"
"os"
"strings"
"testing"
"time"
@ -17,187 +17,328 @@ import (
"github.com/stretchr/testify/assert"
)
func TestGitHubActions(t *testing.T) {
t.Run("BranchBuild", func(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv()
os.Unsetenv("GITHUB_HEAD_REF")
os.Setenv("GITHUB_ACTIONS", "true")
os.Setenv("GITHUB_REF", "refs/heads/feat/test-gh-actions")
os.Setenv("GITHUB_RUN_ID", "42")
os.Setenv("GITHUB_SHA", "abcdef42713")
os.Setenv("GITHUB_SERVER_URL", "github.com")
os.Setenv("GITHUB_REPOSITORY", "foo/bar")
p, _ := NewOrchestratorSpecificConfigProvider()
assert.False(t, p.IsPullRequest())
assert.Equal(t, "github.com/foo/bar/actions/runs/42", p.GetBuildURL())
assert.Equal(t, "feat/test-gh-actions", p.GetBranch())
assert.Equal(t, "refs/heads/feat/test-gh-actions", p.GetReference())
assert.Equal(t, "abcdef42713", p.GetCommit())
assert.Equal(t, "github.com/foo/bar", p.GetRepoURL())
assert.Equal(t, "GitHubActions", p.OrchestratorType())
})
t.Run("PR", func(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv()
os.Setenv("GITHUB_HEAD_REF", "feat/test-gh-actions")
os.Setenv("GITHUB_BASE_REF", "main")
os.Setenv("GITHUB_REF", "refs/pull/42/merge")
p := GitHubActionsConfigProvider{}
c := p.GetPullRequestConfig()
assert.True(t, p.IsPullRequest())
assert.Equal(t, "feat/test-gh-actions", c.Branch)
assert.Equal(t, "main", c.Base)
assert.Equal(t, "42", c.Key)
})
t.Run("Test get logs - success", func(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv()
os.Unsetenv("GITHUB_HEAD_REF")
os.Setenv("GITHUB_ACTIONS", "true")
os.Setenv("GITHUB_REF_NAME", "feat/test-gh-actions")
os.Setenv("GITHUB_REF", "refs/heads/feat/test-gh-actions")
os.Setenv("GITHUB_RUN_ID", "42")
os.Setenv("GITHUB_SHA", "abcdef42713")
os.Setenv("GITHUB_REPOSITORY", "foo/bar")
os.Setenv("GITHUB_URL", "https://github.com/")
p := func() OrchestratorSpecificConfigProviding {
g := GitHubActionsConfigProvider{}
g.client = piperHttp.Client{}
g.client.SetOptions(piperHttp.ClientOptions{
MaxRequestDuration: 5 * time.Second,
Password: "TOKEN",
TransportSkipVerification: true,
UseDefaultTransport: true, // need to use default transport for http mock
MaxRetries: -1,
})
return &g
}()
stagesID := stagesID{
Jobs: []job{
{ID: 123},
{ID: 124},
{ID: 125},
},
}
logs := []string{
"log_record1\n",
"log_record2\n",
}
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(http.MethodGet, "https://api.github.com/repos/foo/bar/actions/runs/42/jobs",
func(req *http.Request) (*http.Response, error) {
return httpmock.NewJsonResponse(200, stagesID)
},
)
httpmock.RegisterResponder(http.MethodGet, "https://api.github.com/repos/foo/bar/actions/jobs/123/logs",
func(req *http.Request) (*http.Response, error) {
return httpmock.NewStringResponse(200, string(logs[0])), nil
},
)
httpmock.RegisterResponder(http.MethodGet, "https://api.github.com/repos/foo/bar/actions/jobs/124/logs",
func(req *http.Request) (*http.Response, error) {
return httpmock.NewStringResponse(200, string(logs[1])), nil
},
)
actual, err := p.GetLog()
assert.NoError(t, err)
assert.Equal(t, strings.Join(logs, ""), string(actual))
})
t.Run("Test get logs - error: failed to get stages ID", func(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv()
os.Unsetenv("GITHUB_HEAD_REF")
os.Setenv("GITHUB_ACTIONS", "true")
os.Setenv("GITHUB_REF_NAME", "feat/test-gh-actions")
os.Setenv("GITHUB_REF", "refs/heads/feat/test-gh-actions")
os.Setenv("GITHUB_RUN_ID", "42")
os.Setenv("GITHUB_SHA", "abcdef42713")
os.Setenv("GITHUB_REPOSITORY", "foo/bar")
os.Setenv("GITHUB_URL", "https://github.com/")
p := func() OrchestratorSpecificConfigProviding {
g := GitHubActionsConfigProvider{}
g.client = piperHttp.Client{}
g.client.SetOptions(piperHttp.ClientOptions{
MaxRequestDuration: 5 * time.Second,
Password: "TOKEN",
TransportSkipVerification: true,
UseDefaultTransport: true, // need to use default transport for http mock
MaxRetries: -1,
})
return &g
}()
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(http.MethodGet, "https://api.github.com/repos/foo/bar/actions/runs/42/jobs",
func(req *http.Request) (*http.Response, error) {
return nil, fmt.Errorf("err")
},
)
actual, err := p.GetLog()
assert.Nil(t, actual)
assert.EqualError(t, err, "failed to get API data: HTTP request to https://api.github.com/repos/foo/bar/actions/runs/42/jobs failed with error: HTTP GET request to https://api.github.com/repos/foo/bar/actions/runs/42/jobs failed: Get \"https://api.github.com/repos/foo/bar/actions/runs/42/jobs\": err")
})
t.Run("Test get logs - failed to get logs", func(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv()
os.Unsetenv("GITHUB_HEAD_REF")
os.Setenv("GITHUB_ACTIONS", "true")
os.Setenv("GITHUB_REF_NAME", "feat/test-gh-actions")
os.Setenv("GITHUB_REF", "refs/heads/feat/test-gh-actions")
os.Setenv("GITHUB_RUN_ID", "42")
os.Setenv("GITHUB_SHA", "abcdef42713")
os.Setenv("GITHUB_REPOSITORY", "foo/bar")
os.Setenv("GITHUB_URL", "https://github.com/")
p := func() OrchestratorSpecificConfigProviding {
g := GitHubActionsConfigProvider{}
g.client = piperHttp.Client{}
g.client.SetOptions(piperHttp.ClientOptions{
MaxRequestDuration: 5 * time.Second,
Password: "TOKEN",
TransportSkipVerification: true,
UseDefaultTransport: true, // need to use default transport for http mock
MaxRetries: -1,
})
return &g
}()
stagesID := stagesID{
Jobs: []job{
{ID: 123},
{ID: 124},
{ID: 125},
},
}
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(http.MethodGet, "https://api.github.com/repos/foo/bar/actions/runs/42/jobs",
func(req *http.Request) (*http.Response, error) {
return httpmock.NewJsonResponse(200, stagesID)
},
)
httpmock.RegisterResponder(http.MethodGet, "https://api.github.com/repos/foo/bar/actions/jobs/123/logs",
func(req *http.Request) (*http.Response, error) {
return nil, fmt.Errorf("err")
},
)
actual, err := p.GetLog()
assert.Nil(t, actual)
// GitHubActionsConfigProvider.GetLog calls http.GetRequest concurrently, so we don't know what log (123 or 124) will be got first
// ref: pkg/orchestrator/gitHubActions.go:90
assert.ErrorContains(t, err, "failed to get logs: failed to get API data: HTTP request to https://api.github.com/repos/foo/bar/actions/jobs/12")
})
func TestGitHubActionsConfigProvider_GetBuildStatus(t *testing.T) {
tests := []struct {
name string
runData run
want string
}{
{"BuildStatusSuccess", run{fetched: true, Status: "success"}, BuildStatusSuccess},
{"BuildStatusAborted", run{fetched: true, Status: "cancelled"}, BuildStatusAborted},
{"BuildStatusInProgress", run{fetched: true, Status: "in_progress"}, BuildStatusInProgress},
{"BuildStatusFailure", run{fetched: true, Status: "qwertyu"}, BuildStatusFailure},
{"BuildStatusFailure", run{fetched: true, Status: ""}, BuildStatusFailure},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := &GitHubActionsConfigProvider{
runData: tt.runData,
}
assert.Equalf(t, tt.want, g.GetBuildStatus(), "GetBuildStatus()")
})
}
}
func TestGitHubActionsConfigProvider_GetBuildReason(t *testing.T) {
tests := []struct {
name string
envGithubRef string
want string
}{
{"BuildReasonManual", "workflow_dispatch", BuildReasonManual},
{"BuildReasonSchedule", "schedule", BuildReasonSchedule},
{"BuildReasonPullRequest", "pull_request", BuildReasonPullRequest},
{"BuildReasonResourceTrigger", "workflow_call", BuildReasonResourceTrigger},
{"BuildReasonIndividualCI", "push", BuildReasonIndividualCI},
{"BuildReasonUnknown", "qwerty", BuildReasonUnknown},
{"BuildReasonUnknown", "", BuildReasonUnknown},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := &GitHubActionsConfigProvider{}
_ = os.Setenv("GITHUB_EVENT_NAME", tt.envGithubRef)
assert.Equalf(t, tt.want, g.GetBuildReason(), "GetBuildReason()")
})
}
}
func TestGitHubActionsConfigProvider_GetRepoURL(t *testing.T) {
tests := []struct {
name string
envServerURL string
envRepo string
want string
}{
{"github.com", "https://github.com", "SAP/jenkins-library", "https://github.com/SAP/jenkins-library"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := &GitHubActionsConfigProvider{}
_ = os.Setenv("GITHUB_SERVER_URL", tt.envServerURL)
_ = os.Setenv("GITHUB_REPOSITORY", tt.envRepo)
assert.Equalf(t, tt.want, g.GetRepoURL(), "GetRepoURL()")
})
}
}
func TestGitHubActionsConfigProvider_GetPullRequestConfig(t *testing.T) {
tests := []struct {
name string
envRef string
want PullRequestConfig
}{
{"1", "refs/pull/1234/merge", PullRequestConfig{"n/a", "n/a", "1234"}},
{"2", "refs/pull/1234", PullRequestConfig{"n/a", "n/a", "1234"}},
{"2", "1234/merge", PullRequestConfig{"n/a", "n/a", "1234"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := &GitHubActionsConfigProvider{}
_ = os.Setenv("GITHUB_REF", tt.envRef)
_ = os.Setenv("GITHUB_HEAD_REF", "n/a")
_ = os.Setenv("GITHUB_BASE_REF", "n/a")
assert.Equalf(t, tt.want, g.GetPullRequestConfig(), "GetPullRequestConfig()")
})
}
}
func TestGitHubActionsConfigProvider_guessCurrentJob(t *testing.T) {
tests := []struct {
name string
jobs []job
jobsFetched bool
targetJobName string
wantJob job
}{
{
name: "job found",
jobs: []job{{Name: "Job1"}, {Name: "Job2"}, {Name: "Job3"}},
jobsFetched: true,
targetJobName: "Job2",
wantJob: job{Name: "Job2"},
},
{
name: "job found",
jobs: []job{{Name: "Piper / Job1"}, {Name: "Piper / Job2"}, {Name: "Piper / Job3"}},
jobsFetched: true,
targetJobName: "Job2",
wantJob: job{Name: "Piper / Job2"},
},
{
name: "job not found",
jobs: []job{{Name: "Job1"}, {Name: "Job2"}, {Name: "Job3"}},
jobsFetched: true,
targetJobName: "Job123",
wantJob: job{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := &GitHubActionsConfigProvider{
jobs: tt.jobs,
jobsFetched: tt.jobsFetched,
}
_ = os.Setenv("GITHUB_JOB", tt.targetJobName)
g.guessCurrentJob()
assert.Equal(t, tt.wantJob, g.currentJob)
})
}
}
func TestGitHubActionsConfigProvider_fetchRunData(t *testing.T) {
// data
respJson := map[string]interface{}{
"status": "completed",
"run_started_at": "2023-08-11T07:28:24Z",
"html_url": "https://github.com/SAP/jenkins-library/actions/runs/11111",
}
startedAt, _ := time.Parse(time.RFC3339, "2023-08-11T07:28:24Z")
wantRunData := run{
fetched: true,
Status: "completed",
StartedAt: startedAt,
}
// setup provider
g := &GitHubActionsConfigProvider{}
g.client.SetOptions(piperHttp.ClientOptions{
UseDefaultTransport: true, // need to use default transport for http mock
MaxRetries: -1,
})
// setup http mock
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(http.MethodGet, "https://api.github.com/repos/SAP/jenkins-library/actions/runs/11111",
func(req *http.Request) (*http.Response, error) {
return httpmock.NewJsonResponse(200, respJson)
},
)
// setup env vars
defer resetEnv(os.Environ())
os.Clearenv()
_ = os.Setenv("GITHUB_API_URL", "https://api.github.com")
_ = os.Setenv("GITHUB_REPOSITORY", "SAP/jenkins-library")
_ = os.Setenv("GITHUB_RUN_ID", "11111")
// run
g.fetchRunData()
assert.Equal(t, wantRunData, g.runData)
}
func TestGitHubActionsConfigProvider_fetchJobs(t *testing.T) {
// data
respJson := map[string]interface{}{"jobs": []map[string]interface{}{{
"id": 111,
"name": "Piper / Init",
"html_url": "https://github.com/SAP/jenkins-library/actions/runs/11111/jobs/111",
}, {
"id": 222,
"name": "Piper / Build",
"html_url": "https://github.com/SAP/jenkins-library/actions/runs/11111/jobs/222",
}, {
"id": 333,
"name": "Piper / Acceptance",
"html_url": "https://github.com/SAP/jenkins-library/actions/runs/11111/jobs/333",
},
}}
wantJobs := []job{{
ID: 111,
Name: "Piper / Init",
HtmlURL: "https://github.com/SAP/jenkins-library/actions/runs/11111/jobs/111",
}, {
ID: 222,
Name: "Piper / Build",
HtmlURL: "https://github.com/SAP/jenkins-library/actions/runs/11111/jobs/222",
}, {
ID: 333,
Name: "Piper / Acceptance",
HtmlURL: "https://github.com/SAP/jenkins-library/actions/runs/11111/jobs/333",
}}
// setup provider
g := &GitHubActionsConfigProvider{}
g.client.SetOptions(piperHttp.ClientOptions{
UseDefaultTransport: true, // need to use default transport for http mock
MaxRetries: -1,
})
// setup http mock
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(
http.MethodGet,
"https://api.github.com/repos/SAP/jenkins-library/actions/runs/11111/jobs",
func(req *http.Request) (*http.Response, error) {
return httpmock.NewJsonResponse(200, respJson)
},
)
// setup env vars
defer resetEnv(os.Environ())
os.Clearenv()
_ = os.Setenv("GITHUB_API_URL", "https://api.github.com")
_ = os.Setenv("GITHUB_REPOSITORY", "SAP/jenkins-library")
_ = os.Setenv("GITHUB_RUN_ID", "11111")
// run
err := g.fetchJobs()
assert.NoError(t, err)
assert.Equal(t, wantJobs, g.jobs)
}
func TestGitHubActionsConfigProvider_GetLog(t *testing.T) {
// data
respLogs := []string{
"log_record11\nlog_record12\nlog_record13\n",
"log_record21\nlog_record22\n",
"log_record31\nlog_record32\n",
"log_record41\n",
}
wantLogs := "log_record11\nlog_record12\nlog_record13\nlog_record21\n" +
"log_record22\nlog_record31\nlog_record32\nlog_record41\n"
jobs := []job{
{ID: 111}, {ID: 222}, {ID: 333}, {ID: 444}, {ID: 555},
}
// setup provider
g := &GitHubActionsConfigProvider{
client: piperHttp.Client{},
jobs: jobs,
jobsFetched: true,
}
g.client.SetOptions(piperHttp.ClientOptions{
UseDefaultTransport: true, // need to use default transport for http mock
MaxRetries: -1,
})
// setup http mock
rand.Seed(time.Now().UnixNano())
latencyMin, latencyMax := 15, 500 // milliseconds
httpmock.Activate()
defer httpmock.DeactivateAndReset()
for i, j := range jobs {
idx := i
httpmock.RegisterResponder(
http.MethodGet,
fmt.Sprintf("https://api.github.com/repos/SAP/jenkins-library/actions/jobs/%d/logs", j.ID),
func(req *http.Request) (*http.Response, error) {
// simulate response delay
latency := rand.Intn(latencyMax-latencyMin) + latencyMin
time.Sleep(time.Duration(latency) * time.Millisecond)
return httpmock.NewStringResponse(200, respLogs[idx]), nil
},
)
}
// setup env vars
defer resetEnv(os.Environ())
os.Clearenv()
_ = os.Setenv("GITHUB_API_URL", "https://api.github.com")
_ = os.Setenv("GITHUB_REPOSITORY", "SAP/jenkins-library")
// run
logs, err := g.GetLog()
assert.NoError(t, err)
assert.Equal(t, wantLogs, string(logs))
}
func TestGitHubActionsConfigProvider_Others(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv()
_ = os.Setenv("GITHUB_ACTION", "1")
_ = os.Setenv("GITHUB_JOB", "Build")
_ = os.Setenv("GITHUB_RUN_ID", "11111")
_ = os.Setenv("GITHUB_REF_NAME", "main")
_ = os.Setenv("GITHUB_HEAD_REF", "feature-branch-1")
_ = os.Setenv("GITHUB_REF", "refs/pull/42/merge")
_ = os.Setenv("GITHUB_WORKFLOW", "Piper workflow")
_ = os.Setenv("GITHUB_SHA", "ffac537e6cbbf934b08745a378932722df287a53")
_ = os.Setenv("GITHUB_API_URL", "https://api.github.com")
_ = os.Setenv("GITHUB_SERVER_URL", "https://github.com")
_ = os.Setenv("GITHUB_REPOSITORY", "SAP/jenkins-library")
p := GitHubActionsConfigProvider{}
startedAt, _ := time.Parse(time.RFC3339, "2023-08-11T07:28:24Z")
p.runData = run{
fetched: true,
Status: "",
StartedAt: startedAt,
}
p.currentJob = job{ID: 111, Name: "job1", HtmlURL: "https://github.com/SAP/jenkins-library/actions/runs/123456/jobs/7654321"}
assert.Equal(t, "n/a", p.OrchestratorVersion())
assert.Equal(t, "GitHubActions", p.OrchestratorType())
assert.Equal(t, "11111", p.GetBuildID())
assert.Equal(t, []ChangeSet{}, p.GetChangeSet())
assert.Equal(t, startedAt, p.GetPipelineStartTime())
assert.Equal(t, "Build", p.GetStageName())
assert.Equal(t, "main", p.GetBranch())
assert.Equal(t, "refs/pull/42/merge", p.GetReference())
assert.Equal(t, "https://github.com/SAP/jenkins-library/actions/runs/11111", p.GetBuildURL())
assert.Equal(t, "https://github.com/SAP/jenkins-library/actions/runs/123456/jobs/7654321", p.GetJobURL())
assert.Equal(t, "Piper workflow", p.GetJobName())
assert.Equal(t, "ffac537e6cbbf934b08745a378932722df287a53", p.GetCommit())
assert.Equal(t, "https://api.github.com/repos/SAP/jenkins-library/actions", actionsURL())
assert.True(t, p.IsPullRequest())
assert.True(t, isGitHubActions())
}

View File

@ -14,20 +14,17 @@ import (
type JenkinsConfigProvider struct {
client piperHttp.Client
options piperHttp.ClientOptions
apiInformation map[string]interface{}
}
// InitOrchestratorProvider initializes the Jenkins orchestrator with credentials
func (j *JenkinsConfigProvider) InitOrchestratorProvider(settings *OrchestratorSettings) {
j.client = piperHttp.Client{}
j.options = piperHttp.ClientOptions{
j.client.SetOptions(piperHttp.ClientOptions{
Username: settings.JenkinsUser,
Password: settings.JenkinsToken,
MaxRetries: 3,
TransportTimeout: time.Second * 10,
}
j.client.SetOptions(j.options)
})
log.Entry().Debug("Successfully initialized Jenkins config provider")
}
@ -77,15 +74,15 @@ func (j *JenkinsConfigProvider) GetBuildStatus() string {
// cases in ADO: succeeded, failed, canceled, none, partiallySucceeded
switch result := val; result {
case "SUCCESS":
return "SUCCESS"
return BuildStatusSuccess
case "ABORTED":
return "ABORTED"
return BuildStatusAborted
default:
// FAILURE, NOT_BUILT
return "FAILURE"
return BuildStatusFailure
}
}
return "FAILURE"
return BuildStatusFailure
}
// GetChangeSet returns the commitIds and timestamp of the changeSet of the current run
@ -200,12 +197,12 @@ func (j *JenkinsConfigProvider) GetBuildReason() string {
marshal, err := json.Marshal(j.apiInformation)
if err != nil {
log.Entry().WithError(err).Debugf("could not marshal apiInformation")
return "Unknown"
return BuildReasonUnknown
}
jsonParsed, err := gabs.ParseJSON(marshal)
if err != nil {
log.Entry().WithError(err).Debugf("could not parse apiInformation")
return "Unknown"
return BuildReasonUnknown
}
for _, child := range jsonParsed.Path("actions").Children() {
@ -217,21 +214,21 @@ func (j *JenkinsConfigProvider) GetBuildReason() string {
for _, val := range child.Path("causes").Children() {
subclass := val.S("_class")
if subclass.Data().(string) == "hudson.model.Cause$UserIdCause" {
return "Manual"
return BuildReasonManual
} else if subclass.Data().(string) == "hudson.triggers.TimerTrigger$TimerTriggerCause" {
return "Schedule"
return BuildReasonSchedule
} else if subclass.Data().(string) == "jenkins.branch.BranchEventCause" {
return "PullRequest"
return BuildReasonPullRequest
} else if subclass.Data().(string) == "org.jenkinsci.plugins.workflow.support.steps.build.BuildUpstreamCause" {
return "ResourceTrigger"
return BuildReasonResourceTrigger
} else {
return "Unknown"
return BuildReasonUnknown
}
}
}
}
return "Unknown"
return BuildReasonUnknown
}
// GetBranch returns the branch name, only works with the git plugin enabled

View File

@ -17,6 +17,20 @@ const (
Jenkins
)
const (
BuildStatusSuccess = "SUCCESS"
BuildStatusAborted = "ABORTED"
BuildStatusFailure = "FAILURE"
BuildStatusInProgress = "IN_PROGRESS"
BuildReasonManual = "Manual"
BuildReasonSchedule = "Schedule"
BuildReasonPullRequest = "PullRequest"
BuildReasonResourceTrigger = "ResourceTrigger"
BuildReasonIndividualCI = "IndividualCI"
BuildReasonUnknown = "Unknown"
)
type OrchestratorSpecificConfigProviding interface {
InitOrchestratorProvider(settings *OrchestratorSettings)
OrchestratorType() string
@ -75,13 +89,13 @@ func NewOrchestratorSpecificConfigProvider() (OrchestratorSpecificConfigProvidin
// DetectOrchestrator returns the name of the current orchestrator e.g. Jenkins, Azure, Unknown
func DetectOrchestrator() Orchestrator {
if isAzure() {
return Orchestrator(AzureDevOps)
return AzureDevOps
} else if isGitHubActions() {
return Orchestrator(GitHubActions)
return GitHubActions
} else if isJenkins() {
return Orchestrator(Jenkins)
return Jenkins
} else {
return Orchestrator(Unknown)
return Unknown
}
}