mirror of
https://github.com/SAP/jenkins-library.git
synced 2024-12-14 11:03:09 +02:00
e805beda70
* 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>
303 lines
8.6 KiB
Go
303 lines
8.6 KiB
Go
package orchestrator
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
piperHttp "github.com/SAP/jenkins-library/pkg/http"
|
|
"github.com/SAP/jenkins-library/pkg/log"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
type GitHubActionsConfigProvider struct {
|
|
client piperHttp.Client
|
|
runData run
|
|
jobs []job
|
|
jobsFetched bool
|
|
currentJob job
|
|
}
|
|
|
|
type run struct {
|
|
fetched bool
|
|
Status string `json:"status"`
|
|
StartedAt time.Time `json:"run_started_at"`
|
|
}
|
|
|
|
type job struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
HtmlURL string `json:"html_url"`
|
|
}
|
|
|
|
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.SetOptions(piperHttp.ClientOptions{
|
|
Token: "Bearer " + settings.GitHubToken,
|
|
MaxRetries: 3,
|
|
TransportTimeout: time.Second * 10,
|
|
})
|
|
|
|
log.Entry().Debug("Successfully initialized GitHubActions config provider")
|
|
}
|
|
|
|
func (g *GitHubActionsConfigProvider) OrchestratorVersion() string {
|
|
log.Entry().Debugf("OrchestratorVersion() for GitHub Actions is not applicable.")
|
|
return "n/a"
|
|
}
|
|
|
|
func (g *GitHubActionsConfigProvider) OrchestratorType() string {
|
|
return "GitHubActions"
|
|
}
|
|
|
|
// GetBuildStatus returns current run status
|
|
func (g *GitHubActionsConfigProvider) GetBuildStatus() string {
|
|
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) {
|
|
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]
|
|
|
|
fullLogs := fullLog{b: make([][]byte, len(jobs))}
|
|
wg := errgroup.Group{}
|
|
wg.SetLimit(10)
|
|
for i := range jobs {
|
|
i := i // https://golang.org/doc/faq#closures_and_goroutines
|
|
wg.Go(func() error {
|
|
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)
|
|
}
|
|
|
|
fullLogs.Lock()
|
|
fullLogs.b[i] = b
|
|
fullLogs.Unlock()
|
|
|
|
return nil
|
|
})
|
|
}
|
|
if err := wg.Wait(); err != nil {
|
|
return nil, fmt.Errorf("failed to get logs: %w", err)
|
|
}
|
|
|
|
return bytes.Join(fullLogs.b, []byte("")), nil
|
|
}
|
|
|
|
// GetBuildID returns current run ID
|
|
func (g *GitHubActionsConfigProvider) GetBuildID() string {
|
|
return getEnv("GITHUB_RUN_ID", "n/a")
|
|
}
|
|
|
|
func (g *GitHubActionsConfigProvider) GetChangeSet() []ChangeSet {
|
|
log.Entry().Debug("GetChangeSet for GitHubActions not implemented")
|
|
return []ChangeSet{}
|
|
}
|
|
|
|
// GetPipelineStartTime returns the pipeline start time in UTC
|
|
func (g *GitHubActionsConfigProvider) GetPipelineStartTime() time.Time {
|
|
g.fetchRunData()
|
|
return g.runData.StartedAt.UTC()
|
|
}
|
|
|
|
// GetStageName returns the human-readable name given to a stage.
|
|
func (g *GitHubActionsConfigProvider) GetStageName() string {
|
|
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 {
|
|
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 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/" + 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 {
|
|
// 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 {
|
|
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")
|
|
prNumber := strings.TrimSuffix(strings.TrimPrefix(githubRef, "refs/pull/"), "/merge")
|
|
return PullRequestConfig{
|
|
Branch: getEnv("GITHUB_HEAD_REF", "n/a"),
|
|
Base: getEnv("GITHUB_BASE_REF", "n/a"),
|
|
Key: prNumber,
|
|
}
|
|
}
|
|
|
|
// IsPullRequest indicates whether the current build is triggered by a PR
|
|
func (g *GitHubActionsConfigProvider) IsPullRequest() bool {
|
|
return truthy("GITHUB_HEAD_REF")
|
|
}
|
|
|
|
func isGitHubActions() bool {
|
|
envVars := []string{"GITHUB_ACTION", "GITHUB_ACTIONS"}
|
|
return areIndicatingEnvVarsSet(envVars)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|