Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/kosli/attestSonar.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ For branch scans: if running the Kosli CLI in some CI/CD pipeline, the revision
or have overridden the revision in SonarQube via parameters to the Sonar scanner, you can provide the correct revision using the ^--sonar-revision^ flag.
If the scan ran on a branch other than the project's main branch in SonarQube, also provide the branch name using the ^--sonar-branch^ flag.
SonarQube only searches the project's main branch unless told otherwise, so without this flag the scan cannot be found.
On SonarQube Cloud, a short-lived branch keeps only its latest analysis, so the revision must be the latest commit analysed on that branch.
For pull request scans: provide the pull-request ID using the ^--pull-request^ flag instead of the revision.
SonarQube keeps only the latest analysis of a pull request, so if the scan for the current push has not finished, that is the previous push's analysis.
To make sure the attested analysis is of the commit you expect, also pass ^--sonar-revision^: the command then fails if the pull request's analysis is of another commit.
Expand Down
109 changes: 96 additions & 13 deletions internal/sonar/sonar.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,18 @@ type PRCommit struct {
SHA string `json:"sha"`
}

// These are the structs for the response from the project_branches/list API
type BranchesResponse struct {
Branches []BranchInfo `json:"branches"`
}

type BranchInfo struct {
Name string `json:"name"`
Type string `json:"type"`
AnalysisDate string `json:"analysisDate"`
Commit PRCommit `json:"commit"`
}

// Struct for error messages from sonar APIs
type Error struct {
Msg string `json:"msg"`
Expand Down Expand Up @@ -271,6 +283,20 @@ func (sc *SonarConfig) GetSonarResults(logger *log.Logger) (*SonarResults, error
if err != nil {
return nil, err
}
// A short-lived branch's gate is its latest, so a push processed since the
// revision was matched would have replaced it with the newer commit's.
if gateIsBranchLatest(sonarResults, analysisID) {
branch, err := listedShortLivedBranch(httpClient, sonarResults, project)
if err == nil && branch == nil {
err = fmt.Errorf("the branch is no longer listed")
}
if err != nil {
return nil, fmt.Errorf("could not confirm that the latest analysis of short-lived branch %s of project %s is still of revision %s, so its quality gate may be of another commit: %w", sonarResults.Branch.Name, project.Key, sonarResults.Revision, err)
}
if branch.Commit.SHA != sonarResults.Revision {
return nil, fmt.Errorf("a newer analysis, of revision %s, replaced the analysis of revision %s on short-lived branch %s of project %s while it was being attested.\nSonarQube Cloud keeps only the latest analysis of a short-lived branch, so revision %s can no longer be attested", branch.Commit.SHA, sonarResults.Revision, branch.Name, project.Key, sonarResults.Revision)
}
}

sonarResults.Project = *project
sonarResults.QualityGate = qualityGate
Expand Down Expand Up @@ -432,7 +458,15 @@ func GetProjectAnalysisFromRevision(httpClient *http.Client, sonarResults *Sonar
}

if projectAnalysesData.Errors != nil {
return "", fmt.Errorf("SonarQube error: %s", projectAnalysesData.Errors[0].Msg)
sonarErr := fmt.Errorf("SonarQube error: %s", projectAnalysesData.Errors[0].Msg)
if sonarResults.Branch == nil {
return "", sonarErr
}
isShort, err := shortLivedBranchAnalysis(httpClient, sonarResults, project, revision)
if !isShort {
return "", sonarErr
}
return "", err
}

for analysis := range projectAnalysesData.Analyses {
Expand All @@ -456,6 +490,58 @@ func GetProjectAnalysisFromRevision(httpClient *http.Client, sonarResults *Sonar
return analysisID, nil
}

// gateIsBranchLatest reports whether the quality gate can only be read as the
// branch's latest. Only a short-lived branch is found without an analysis ID.
func gateIsBranchLatest(sonarResults *SonarResults, analysisID string) bool {
return analysisID == "" && sonarResults.Branch != nil
}

// shortLivedBranchAnalysis finds the analysis of revision on a SonarCloud short-lived
// branch, which api/project_analyses/search refuses. SonarCloud keeps only the latest
// analysis of such a branch, so that analysis must be of the revision. It reports
// false, so the caller keeps SonarQube's own error, when the branch cannot be looked
// up or is not a listed short-lived branch.
func shortLivedBranchAnalysis(httpClient *http.Client, sonarResults *SonarResults, project *Project, revision string) (bool, error) {
branch, err := listedShortLivedBranch(httpClient, sonarResults, project)
if err != nil || branch == nil {
return false, nil
}
if branch.Commit.SHA == "" {
return true, fmt.Errorf("analysis for revision %s of project %s not found: branch %s is short-lived on SonarQube Cloud and has no analysis yet.\nThe scan may still be being processed by SonarQube, try again later", revision, project.Key, branch.Name)
}
if branch.Commit.SHA != revision {
return true, fmt.Errorf("analysis for revision %s of project %s not found: branch %s is short-lived on SonarQube Cloud, which keeps only its latest analysis, and that is of revision %s.\nThe scan for your revision may still be being processed by SonarQube, try again later.\nOtherwise check the revision is correct, or analyse the branch as long-lived: SonarQube Cloud decides this from the branch name when the branch is first analysed", revision, project.Key, branch.Name, branch.Commit.SHA)
}
sonarResults.AnalysedAt = branch.AnalysisDate
sonarResults.Branch.Type = branch.Type
return true, nil
}

// listedShortLivedBranch returns the branch from api/project_branches/list when
// SonarCloud lists it as short-lived, and nil when it does not.
func listedShortLivedBranch(httpClient *http.Client, sonarResults *SonarResults, project *Project) (*BranchInfo, error) {
branchesURL, err := sonarURL(sonarResults.ServerUrl, "api/project_branches/list", url.Values{"project": {project.Key}})
if err != nil {
return nil, err
}
branchesResponse, err := httpClient.Get(branchesURL)
if err != nil {
return nil, err
}
defer func() { _ = branchesResponse.Body.Close() }()

branchesData := &BranchesResponse{}
if branchesResponse.StatusCode/100 != 2 || json.NewDecoder(branchesResponse.Body).Decode(branchesData) != nil {
return nil, sonarResponseError(branchesResponse.StatusCode)
}
for _, branch := range branchesData.Branches {
if branch.Name == sonarResults.Branch.Name && branch.Type == "SHORT" {
return &branch, nil
}
}
return nil, nil
}

func GetProjectAnalysisFromAnalysisID(httpClient *http.Client, sonarResults *SonarResults, project *Project, analysisID string) error {
projectAnalysesURL, err := analysesSearchURL(sonarResults, project)
if err != nil {
Expand Down Expand Up @@ -538,18 +624,15 @@ func GetPRAnalysisData(httpClient *http.Client, sonarResults *SonarResults, proj
}

func GetQualityGate(httpClient *http.Client, sonarResults *SonarResults, qualityGate *QualityGate, analysisID, projectKey, pullRequest string) (*QualityGate, error) {
var qualityGateURL string
var err error
if pullRequest == "" {
qualityGateURL, err = sonarURL(sonarResults.ServerUrl, "api/qualitygates/project_status", url.Values{"analysisId": {analysisID}})
if err != nil {
return nil, err
}
} else {
qualityGateURL, err = sonarURL(sonarResults.ServerUrl, "api/qualitygates/project_status", url.Values{"projectKey": {projectKey}, "pullRequest": {pullRequest}})
if err != nil {
return nil, err
}
params := url.Values{"analysisId": {analysisID}}
if pullRequest != "" {
params = url.Values{"projectKey": {projectKey}, "pullRequest": {pullRequest}}
} else if gateIsBranchLatest(sonarResults, analysisID) {
params = url.Values{"projectKey": {projectKey}, "branch": {sonarResults.Branch.Name}}
}
qualityGateURL, err := sonarURL(sonarResults.ServerUrl, "api/qualitygates/project_status", params)
if err != nil {
return nil, err
}
qualityGateRequest, err := http.NewRequest("GET", qualityGateURL, nil)
if err != nil {
Expand Down
Loading
Loading