1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-14 11:03:09 +02:00
sap-jenkins-library/pkg/orchestrator/gitHubActions.go
Vyacheslav Starostin 9aac0ae06c
GHActions: Add the possibility to fetch logs (#4250)
* Initial commit

* getStageId added

* setting token for orchestrator

* Add possibility to fetch logs (GHActions)

* Update

* Clean up: delete test log file

* go mod tidy

* Rename token

* Rename token

* Apply suggestion from code review

* Update

* Correct err msg

* Add new line

* Update pkg/orchestrator/gitHubActions.go

Co-authored-by: ffeldmann <f.feldmann@sap.com>

* Update pkg/orchestrator/gitHubActions.go

Co-authored-by: ffeldmann <f.feldmann@sap.com>

* Update pkg/orchestrator/gitHubActions.go

Co-authored-by: ffeldmann <f.feldmann@sap.com>

* Add httpHeader variable && little updates

* Clean up

* Make structs unexported

---------

Co-authored-by: Aibyn Sadu <aibaend@yandex.ru>
Co-authored-by: ffeldmann <f.feldmann@sap.com>
2023-03-01 10:38:40 +06:00

217 lines
5.8 KiB
Go

package orchestrator
import (
"bytes"
"context"
"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"
"golang.org/x/sync/semaphore"
)
var httpHeader = http.Header{
"Accept": {"application/vnd.github+json"},
}
type GitHubActionsConfigProvider struct {
client piperHttp.Client
}
type job struct {
ID int `json:"id"`
}
type stagesID struct {
Jobs []job `json:"jobs"`
}
type logs struct {
sync.Mutex
b [][]byte
}
// InitOrchestratorProvider initializes http client for GitHubActionsDevopsConfigProvider
func (g *GitHubActionsConfigProvider) InitOrchestratorProvider(settings *OrchestratorSettings) {
g.client = piperHttp.Client{}
g.client.SetOptions(piperHttp.ClientOptions{
Password: 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 {
return "n/a"
}
func (g *GitHubActionsConfigProvider) OrchestratorType() string {
return "GitHubActions"
}
func (g *GitHubActionsConfigProvider) GetBuildStatus() string {
log.Entry().Infof("GetBuildStatus() for GitHub Actions not yet implemented.")
return "FAILURE"
}
// GetLog returns the whole logfile for the current pipeline run
func (g *GitHubActionsConfigProvider) GetLog() ([]byte, error) {
ids, err := g.getStageIds()
if err != nil {
return nil, err
}
logs := logs{
b: make([][]byte, len(ids)),
}
ctx := context.Background()
sem := semaphore.NewWeighted(10)
wg := errgroup.Group{}
for i := range ids {
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)
if err != nil {
return fmt.Errorf("failed to get API data: %w", err)
}
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
return nil
})
}
if err = wg.Wait(); err != nil {
return nil, fmt.Errorf("failed to get logs: %w", err)
}
return bytes.Join(logs.b, []byte("")), nil
}
func (g *GitHubActionsConfigProvider) GetBuildID() string {
log.Entry().Infof("GetBuildID() for GitHub Actions not yet implemented.")
return "n/a"
}
func (g *GitHubActionsConfigProvider) GetChangeSet() []ChangeSet {
log.Entry().Warn("GetChangeSet for GitHubActions not yet implemented")
return []ChangeSet{}
}
func (g *GitHubActionsConfigProvider) GetPipelineStartTime() time.Time {
log.Entry().Infof("GetPipelineStartTime() for GitHub Actions not yet implemented.")
return time.Time{}.UTC()
}
func (g *GitHubActionsConfigProvider) GetStageName() string {
return "GITHUB_WORKFLOW" // TODO: is there something like is "stage" in GH Actions?
}
func (g *GitHubActionsConfigProvider) GetBuildReason() string {
log.Entry().Infof("GetBuildReason() for GitHub Actions not yet implemented.")
return "n/a"
}
func (g *GitHubActionsConfigProvider) GetBranch() string {
return strings.TrimPrefix(getEnv("GITHUB_REF", "n/a"), "refs/heads/")
}
func (g *GitHubActionsConfigProvider) GetReference() string {
return getEnv("GITHUB_REF", "n/a")
}
func (g *GitHubActionsConfigProvider) GetBuildURL() string {
return g.GetRepoURL() + "/actions/runs/" + getEnv("GITHUB_RUN_ID", "n/a")
}
func (g *GitHubActionsConfigProvider) GetJobURL() string {
log.Entry().Debugf("Not yet implemented.")
return g.GetRepoURL() + "/actions/runs/" + getEnv("GITHUB_RUN_ID", "n/a")
}
func (g *GitHubActionsConfigProvider) GetJobName() string {
log.Entry().Debugf("GetJobName() for GitHubActions not yet implemented.")
return "n/a"
}
func (g *GitHubActionsConfigProvider) GetCommit() string {
return getEnv("GITHUB_SHA", "n/a")
}
func (g *GitHubActionsConfigProvider) GetRepoURL() string {
return getEnv("GITHUB_SERVER_URL", "n/a") + "/" + getEnv("GITHUB_REPOSITORY", "n/a")
}
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,
}
}
func (g *GitHubActionsConfigProvider) IsPullRequest() bool {
return truthy("GITHUB_HEAD_REF")
}
func isGitHubActions() bool {
envVars := []string{"GITHUB_ACTION", "GITHUB_ACTIONS"}
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
}