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
63 changes: 63 additions & 0 deletions artifactory/utils/commandsummary/evidence_url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package commandsummary

import (
"fmt"
"net/url"
)

const (
releaseBundleEvidenceFormat = "%sui/artifactory/lifecycle?range=Any+Time&bundleName=%s&repositoryKey=%s&releaseBundleVersion=%s&activeVersionTab=Evidence+Graph"
buildEvidenceFormat = "%sui/builds/%s/%s/%s/Evidence/%s?buildRepo=%s"
artifactEvidenceFormat = "%sui/repos/tree/Evidence/%s?clearFilter=true"
)

func GenerateEvidenceUrlByType(data EvidenceSummaryData, section summarySection) (string, error) {
switch data.SubjectType {
// Currently, it is not possible to generate a link to the evidence tab for packages in the Artifactory UI.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not ok with merging this comment on an open source.

// The link will point to the lead artifact of the package instead.
// This logic will be updated once UI support is available
case SubjectTypePackage, SubjectTypeArtifact:
return generateArtifactEvidenceUrl(data.Subject, section)
case SubjectTypeReleaseBundle:
return generateReleaseBundleEvidenceUrl(data, section)
case SubjectTypeBuild:
return generateBuildEvidenceUrl(data, section)
default:
return generateArtifactEvidenceUrl(data.Subject, section)
}
}

func generateArtifactEvidenceUrl(pathInRt string, section summarySection) (string, error) {
urlStr := fmt.Sprintf(artifactEvidenceFormat, StaticMarkdownConfig.GetPlatformUrl(), pathInRt)
return addGitHubTrackingToUrl(urlStr, section)
}

func generateReleaseBundleEvidenceUrl(data EvidenceSummaryData, section summarySection) (string, error) {
if data.ReleaseBundleName == "" || data.ReleaseBundleVersion == "" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explain this use case? when do we find release bundle type without release bundle name and release bundle version

return generateArtifactEvidenceUrl(data.Subject, section)
}

urlStr := fmt.Sprintf(releaseBundleEvidenceFormat,
StaticMarkdownConfig.GetPlatformUrl(),
data.ReleaseBundleName,
data.RepoKey,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the repokey in this case?
-release-bundle-v2?

data.ReleaseBundleVersion)

return addGitHubTrackingToUrl(urlStr, section)
}

func generateBuildEvidenceUrl(data EvidenceSummaryData, section summarySection) (string, error) {
if data.BuildName == "" || data.BuildNumber == "" || data.BuildTimestamp == "" {
return generateArtifactEvidenceUrl(data.Subject, section)
}

urlStr := fmt.Sprintf(buildEvidenceFormat,
StaticMarkdownConfig.GetPlatformUrl(),
url.QueryEscape(data.BuildName),
data.BuildNumber,
data.BuildTimestamp,
url.QueryEscape(data.BuildName),
data.RepoKey)

return addGitHubTrackingToUrl(urlStr, section)
}
141 changes: 141 additions & 0 deletions artifactory/utils/commandsummary/evidence_url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package commandsummary

import (
"os"
"testing"

"github.com/stretchr/testify/assert"
)

func TestGenerateEvidenceUrlByType(t *testing.T) {
// Set up test environment
originalWorkflow := os.Getenv(workflowEnvKey)
err := os.Setenv(workflowEnvKey, "JFrog CLI Core Tests")
if err != nil {
assert.FailNow(t, "Failed to set environment variable", err)
}
defer func() {
if originalWorkflow != "" {
err = os.Setenv(workflowEnvKey, originalWorkflow)
if err != nil {
assert.Fail(t, "Failed to restore workflow environment variable", err)
return
}
} else {
os.Unsetenv(workflowEnvKey)
}
}()

// Configure static markdown config for tests
StaticMarkdownConfig.setPlatformUrl("https://myplatform.com/")
StaticMarkdownConfig.setPlatformMajorVersion(7)

tests := []struct {
name string
data EvidenceSummaryData
expectedURL string
expectError bool
}{
{
name: "Package evidence URL",
data: EvidenceSummaryData{
Subject: "repo/path/package.jar",
SubjectType: SubjectTypePackage,
},
expectedURL: "https://myplatform.com/ui/repos/tree/Evidence/repo/path/package.jar?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=evidence&m=3&s=1",
},
{
name: "Artifact evidence URL",
data: EvidenceSummaryData{
Subject: "repo/path/artifact.txt",
SubjectType: SubjectTypeArtifact,
},
expectedURL: "https://myplatform.com/ui/repos/tree/Evidence/repo/path/artifact.txt?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=evidence&m=3&s=1",
},
{
name: "Release bundle evidence URL",
data: EvidenceSummaryData{
Subject: "release-bundles-v2/my-bundle/1.0.0/release-bundle.json.evd",
SubjectType: SubjectTypeReleaseBundle,
ReleaseBundleName: "my-bundle",
ReleaseBundleVersion: "1.0.0",
RepoKey: "release-bundles-v2",
},
expectedURL: "", // Will be checked with custom assertion
},
{
name: "Build evidence URL",
data: EvidenceSummaryData{
Subject: "artifactory-build-info/my-build/123/1234567890.json",
SubjectType: SubjectTypeBuild,
BuildName: "my-build",
BuildNumber: "123",
BuildTimestamp: "1234567890",
RepoKey: "artifactory-build-info",
},
expectedURL: "https://myplatform.com/ui/builds/my-build/123/1234567890/Evidence/my-build?buildRepo=artifactory-build-info&gh_job_id=JFrog+CLI+Core+Tests&gh_section=evidence&m=3&s=1",
},
{
name: "Build with special characters in name",
data: EvidenceSummaryData{
Subject: "artifactory-build-info/my build with spaces/123/1234567890.json",
SubjectType: SubjectTypeBuild,
BuildName: "my build with spaces",
BuildNumber: "123",
BuildTimestamp: "1234567890",
RepoKey: "artifactory-build-info",
},
expectedURL: "https://myplatform.com/ui/builds/my+build+with+spaces/123/1234567890/Evidence/my+build+with+spaces?buildRepo=artifactory-build-info&gh_job_id=JFrog+CLI+Core+Tests&gh_section=evidence&m=3&s=1",
},
{
name: "Invalid release bundle falls back to artifact URL",
data: EvidenceSummaryData{
Subject: "invalid/path",
SubjectType: SubjectTypeReleaseBundle,
},
expectedURL: "https://myplatform.com/ui/repos/tree/Evidence/invalid/path?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=evidence&m=3&s=1",
},
{
name: "Invalid build falls back to artifact URL",
data: EvidenceSummaryData{
Subject: "invalid/build/path",
SubjectType: SubjectTypeBuild,
},
expectedURL: "https://myplatform.com/ui/repos/tree/Evidence/invalid/build/path?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=evidence&m=3&s=1",
},
{
name: "Default type uses artifact URL",
data: EvidenceSummaryData{
Subject: "some/path/file.txt",
SubjectType: "", // Empty type should default to artifact
},
expectedURL: "https://myplatform.com/ui/repos/tree/Evidence/some/path/file.txt?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=evidence&m=3&s=1",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
url, err := GenerateEvidenceUrlByType(tt.data, evidenceSection)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
if tt.expectedURL != "" {
assert.Equal(t, tt.expectedURL, url)
} else if tt.name == "Release bundle evidence URL" {
// Special handling for release bundle URL due to parameter ordering
assert.Contains(t, url, "https://myplatform.com/ui/artifactory/lifecycle?")
assert.Contains(t, url, "bundleName=my-bundle")
assert.Contains(t, url, "repositoryKey=release-bundles-v2")
assert.Contains(t, url, "releaseBundleVersion=1.0.0")
assert.Contains(t, url, "activeVersionTab=Evidence+Graph")
assert.Contains(t, url, "gh_job_id=JFrog+CLI+Core+Tests")
assert.Contains(t, url, "gh_section=evidence")
assert.Contains(t, url, "m=3")
assert.Contains(t, url, "s=1")
assert.Contains(t, url, "range=Any+Time")
}
}
})
}
}
151 changes: 151 additions & 0 deletions artifactory/utils/commandsummary/evidencesummary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package commandsummary

import (
"fmt"
"github.com/jfrog/jfrog-client-go/utils/log"
"strings"
"time"
)

const evidenceHeaderSize = 3

type EvidenceSummaryData struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to have it here?
It creates decoupling each time we will want to add a field we will have to go here as well.

Subject string `json:"subject"`
SubjectSha256 string `json:"subjectSha256"`
PredicateType string `json:"predicateType"`
PredicateSlug string `json:"predicateSlug"`
Verified bool `json:"verified"`
DisplayName string `json:"displayName,omitempty"`
SubjectType SubjectType `json:"subjectType"`
BuildName string `json:"buildName"`
BuildNumber string `json:"buildNumber"`
BuildTimestamp string `json:"buildTimestamp"`
ReleaseBundleName string `json:"releaseBundleName"`
ReleaseBundleVersion string `json:"releaseBundleVersion"`
RepoKey string `json:"repoKey"`
CreatedAt time.Time `json:"createdAt"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreatedBy? ProviderId?

}

type SubjectType string

const (
SubjectTypeArtifact SubjectType = "artifact"
SubjectTypeBuild SubjectType = "build"
SubjectTypePackage SubjectType = "package"
SubjectTypeReleaseBundle SubjectType = "release-bundle"
)

type EvidenceSummary struct {
CommandSummary
}

func NewEvidenceSummary() (*CommandSummary, error) {
return New(&EvidenceSummary{}, "evidence")
}

func (es *EvidenceSummary) GetSummaryTitle() string {
return "🔎 Evidence"
}

func (es *EvidenceSummary) GenerateMarkdownFromFiles(dataFilePaths []string) (finalMarkdown string, err error) {
log.Debug("Generating evidence summary markdown.")
var evidenceData []EvidenceSummaryData
for _, filePath := range dataFilePaths {
var evidence EvidenceSummaryData
if err = UnmarshalFromFilePath(filePath, &evidence); err != nil {
log.Warn("Failed to unmarshal evidence data from file %s: %v", filePath, err)
return
}
evidenceData = append(evidenceData, evidence)
}

if len(evidenceData) == 0 {
return
}

tableMarkdown := es.generateEvidenceTable(evidenceData)
return WrapCollapsableMarkdown(es.GetSummaryTitle(), tableMarkdown, evidenceHeaderSize), nil
}

func (es *EvidenceSummary) generateEvidenceTable(evidenceData []EvidenceSummaryData) string {
var tableBuilder strings.Builder
tableBuilder.WriteString(es.getTableHeader())

for _, evidence := range evidenceData {
es.appendEvidenceRow(&tableBuilder, evidence)
}

tableBuilder.WriteString("</tbody></table> \n")
return tableBuilder.String()
}

func (es *EvidenceSummary) getTableHeader() string {
return "<table><thead><tr><th>Evidence Subject</th><th>Evidence Type</th><th>Verification Status</th></tr></thead><tbody>\n"
}

func (es *EvidenceSummary) appendEvidenceRow(tableBuilder *strings.Builder, evidence EvidenceSummaryData) {
subject := es.formatSubjectWithLink(evidence)
evidenceType := es.formatEvidenceType(evidence)
verificationStatus := es.formatVerificationStatus(evidence.Verified)

tableBuilder.WriteString(fmt.Sprintf("<tr><td>%s</td><td>%s</td><td>%s</td></tr>\n", subject, evidenceType, verificationStatus))
}

func (es *EvidenceSummary) formatSubjectWithLink(evidence EvidenceSummaryData) string {
if evidence.Subject == "" {
return "evidence"
}

evidenceUrl, err := GenerateEvidenceUrlByType(evidence, evidenceSection)
if err != nil {
log.Warn("Failed to generate evidence URL: %v", err)
evidenceUrl = ""
}

displayName := evidence.DisplayName
if displayName == "" {
displayName = evidence.Subject
}

var viewLink string
subjectType := es.formatSubjectType(evidence.SubjectType)
if evidenceUrl != "" {
viewLink = fmt.Sprintf(`%s <a href="%s">%s</a>`, subjectType, evidenceUrl, displayName)
} else {
viewLink = fmt.Sprintf("%s %s", subjectType, displayName)
}

return viewLink
}

func (es *EvidenceSummary) formatVerificationStatus(verified bool) string {
if verified {
return fmt.Sprintf("%s Verified", "✅")
}
return fmt.Sprintf("%s Not Verified", "❌")
}

func (es *EvidenceSummary) formatEvidenceType(evidence EvidenceSummaryData) string {
if evidence.PredicateSlug == "" {
if evidence.PredicateType == "" {
return "⚠️ Unknown"
}
return evidence.PredicateType
}
return evidence.PredicateSlug
}

func (es *EvidenceSummary) formatSubjectType(subjectType SubjectType) string {
switch subjectType {
case SubjectTypePackage:
return "📦️"
case SubjectTypeBuild:
return "🛠️️"
case SubjectTypeReleaseBundle:
return "🧩"
case SubjectTypeArtifact:
return "📄"
default:
return ""
}
}
Loading
Loading