Skip to content

Commit 9cf424e

Browse files
authored
Add GitHub Actions summary for the create evidence command (#1423)
1 parent a4b0f67 commit 9cf424e

File tree

7 files changed

+553
-0
lines changed

7 files changed

+553
-0
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package commandsummary
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
)
7+
8+
const (
9+
releaseBundleEvidenceFormat = "%sui/artifactory/lifecycle?range=Any+Time&bundleName=%s&repositoryKey=%s&releaseBundleVersion=%s&activeVersionTab=Evidence+Graph"
10+
buildEvidenceFormat = "%sui/builds/%s/%s/%s/Evidence/%s?buildRepo=%s"
11+
artifactEvidenceFormat = "%sui/repos/tree/Evidence/%s?clearFilter=true"
12+
)
13+
14+
func GenerateEvidenceUrlByType(data EvidenceSummaryData, section summarySection) (string, error) {
15+
switch data.SubjectType {
16+
// Currently, it is not possible to generate a link to the evidence tab for packages in the Artifactory UI.
17+
// The link will point to the lead artifact of the package instead.
18+
// This logic will be updated once UI support is available
19+
case SubjectTypePackage, SubjectTypeArtifact:
20+
return generateArtifactEvidenceUrl(data.Subject, section)
21+
case SubjectTypeReleaseBundle:
22+
return generateReleaseBundleEvidenceUrl(data, section)
23+
case SubjectTypeBuild:
24+
return generateBuildEvidenceUrl(data, section)
25+
default:
26+
return generateArtifactEvidenceUrl(data.Subject, section)
27+
}
28+
}
29+
30+
func generateArtifactEvidenceUrl(pathInRt string, section summarySection) (string, error) {
31+
urlStr := fmt.Sprintf(artifactEvidenceFormat, StaticMarkdownConfig.GetPlatformUrl(), pathInRt)
32+
return addGitHubTrackingToUrl(urlStr, section)
33+
}
34+
35+
func generateReleaseBundleEvidenceUrl(data EvidenceSummaryData, section summarySection) (string, error) {
36+
if data.ReleaseBundleName == "" || data.ReleaseBundleVersion == "" {
37+
return generateArtifactEvidenceUrl(data.Subject, section)
38+
}
39+
40+
urlStr := fmt.Sprintf(releaseBundleEvidenceFormat,
41+
StaticMarkdownConfig.GetPlatformUrl(),
42+
data.ReleaseBundleName,
43+
data.RepoKey,
44+
data.ReleaseBundleVersion)
45+
46+
return addGitHubTrackingToUrl(urlStr, section)
47+
}
48+
49+
func generateBuildEvidenceUrl(data EvidenceSummaryData, section summarySection) (string, error) {
50+
if data.BuildName == "" || data.BuildNumber == "" || data.BuildTimestamp == "" {
51+
return generateArtifactEvidenceUrl(data.Subject, section)
52+
}
53+
54+
urlStr := fmt.Sprintf(buildEvidenceFormat,
55+
StaticMarkdownConfig.GetPlatformUrl(),
56+
url.QueryEscape(data.BuildName),
57+
data.BuildNumber,
58+
data.BuildTimestamp,
59+
url.QueryEscape(data.BuildName),
60+
data.RepoKey)
61+
62+
return addGitHubTrackingToUrl(urlStr, section)
63+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package commandsummary
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestGenerateEvidenceUrlByType(t *testing.T) {
11+
// Set up test environment
12+
originalWorkflow := os.Getenv(workflowEnvKey)
13+
err := os.Setenv(workflowEnvKey, "JFrog CLI Core Tests")
14+
if err != nil {
15+
assert.FailNow(t, "Failed to set environment variable", err)
16+
}
17+
defer func() {
18+
if originalWorkflow != "" {
19+
err = os.Setenv(workflowEnvKey, originalWorkflow)
20+
if err != nil {
21+
assert.Fail(t, "Failed to restore workflow environment variable", err)
22+
return
23+
}
24+
} else {
25+
os.Unsetenv(workflowEnvKey)
26+
}
27+
}()
28+
29+
// Configure static markdown config for tests
30+
StaticMarkdownConfig.setPlatformUrl("https://myplatform.com/")
31+
StaticMarkdownConfig.setPlatformMajorVersion(7)
32+
33+
tests := []struct {
34+
name string
35+
data EvidenceSummaryData
36+
expectedURL string
37+
expectError bool
38+
}{
39+
{
40+
name: "Package evidence URL",
41+
data: EvidenceSummaryData{
42+
Subject: "repo/path/package.jar",
43+
SubjectType: SubjectTypePackage,
44+
},
45+
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",
46+
},
47+
{
48+
name: "Artifact evidence URL",
49+
data: EvidenceSummaryData{
50+
Subject: "repo/path/artifact.txt",
51+
SubjectType: SubjectTypeArtifact,
52+
},
53+
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",
54+
},
55+
{
56+
name: "Release bundle evidence URL",
57+
data: EvidenceSummaryData{
58+
Subject: "release-bundles-v2/my-bundle/1.0.0/release-bundle.json.evd",
59+
SubjectType: SubjectTypeReleaseBundle,
60+
ReleaseBundleName: "my-bundle",
61+
ReleaseBundleVersion: "1.0.0",
62+
RepoKey: "release-bundles-v2",
63+
},
64+
expectedURL: "", // Will be checked with custom assertion
65+
},
66+
{
67+
name: "Build evidence URL",
68+
data: EvidenceSummaryData{
69+
Subject: "artifactory-build-info/my-build/123/1234567890.json",
70+
SubjectType: SubjectTypeBuild,
71+
BuildName: "my-build",
72+
BuildNumber: "123",
73+
BuildTimestamp: "1234567890",
74+
RepoKey: "artifactory-build-info",
75+
},
76+
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",
77+
},
78+
{
79+
name: "Build with special characters in name",
80+
data: EvidenceSummaryData{
81+
Subject: "artifactory-build-info/my build with spaces/123/1234567890.json",
82+
SubjectType: SubjectTypeBuild,
83+
BuildName: "my build with spaces",
84+
BuildNumber: "123",
85+
BuildTimestamp: "1234567890",
86+
RepoKey: "artifactory-build-info",
87+
},
88+
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",
89+
},
90+
{
91+
name: "Invalid release bundle falls back to artifact URL",
92+
data: EvidenceSummaryData{
93+
Subject: "invalid/path",
94+
SubjectType: SubjectTypeReleaseBundle,
95+
},
96+
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",
97+
},
98+
{
99+
name: "Invalid build falls back to artifact URL",
100+
data: EvidenceSummaryData{
101+
Subject: "invalid/build/path",
102+
SubjectType: SubjectTypeBuild,
103+
},
104+
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",
105+
},
106+
{
107+
name: "Default type uses artifact URL",
108+
data: EvidenceSummaryData{
109+
Subject: "some/path/file.txt",
110+
SubjectType: "", // Empty type should default to artifact
111+
},
112+
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",
113+
},
114+
}
115+
116+
for _, tt := range tests {
117+
t.Run(tt.name, func(t *testing.T) {
118+
url, err := GenerateEvidenceUrlByType(tt.data, evidenceSection)
119+
if tt.expectError {
120+
assert.Error(t, err)
121+
} else {
122+
assert.NoError(t, err)
123+
if tt.expectedURL != "" {
124+
assert.Equal(t, tt.expectedURL, url)
125+
} else if tt.name == "Release bundle evidence URL" {
126+
// Special handling for release bundle URL due to parameter ordering
127+
assert.Contains(t, url, "https://myplatform.com/ui/artifactory/lifecycle?")
128+
assert.Contains(t, url, "bundleName=my-bundle")
129+
assert.Contains(t, url, "repositoryKey=release-bundles-v2")
130+
assert.Contains(t, url, "releaseBundleVersion=1.0.0")
131+
assert.Contains(t, url, "activeVersionTab=Evidence+Graph")
132+
assert.Contains(t, url, "gh_job_id=JFrog+CLI+Core+Tests")
133+
assert.Contains(t, url, "gh_section=evidence")
134+
assert.Contains(t, url, "m=3")
135+
assert.Contains(t, url, "s=1")
136+
assert.Contains(t, url, "range=Any+Time")
137+
}
138+
}
139+
})
140+
}
141+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package commandsummary
2+
3+
import (
4+
"fmt"
5+
"github.com/jfrog/jfrog-client-go/utils/log"
6+
"strings"
7+
"time"
8+
)
9+
10+
const evidenceHeaderSize = 3
11+
12+
type EvidenceSummaryData struct {
13+
Subject string `json:"subject"`
14+
SubjectSha256 string `json:"subjectSha256"`
15+
PredicateType string `json:"predicateType"`
16+
PredicateSlug string `json:"predicateSlug"`
17+
Verified bool `json:"verified"`
18+
DisplayName string `json:"displayName,omitempty"`
19+
SubjectType SubjectType `json:"subjectType"`
20+
BuildName string `json:"buildName"`
21+
BuildNumber string `json:"buildNumber"`
22+
BuildTimestamp string `json:"buildTimestamp"`
23+
ReleaseBundleName string `json:"releaseBundleName"`
24+
ReleaseBundleVersion string `json:"releaseBundleVersion"`
25+
RepoKey string `json:"repoKey"`
26+
CreatedAt time.Time `json:"createdAt"`
27+
}
28+
29+
type SubjectType string
30+
31+
const (
32+
SubjectTypeArtifact SubjectType = "artifact"
33+
SubjectTypeBuild SubjectType = "build"
34+
SubjectTypePackage SubjectType = "package"
35+
SubjectTypeReleaseBundle SubjectType = "release-bundle"
36+
)
37+
38+
type EvidenceSummary struct {
39+
CommandSummary
40+
}
41+
42+
func NewEvidenceSummary() (*CommandSummary, error) {
43+
return New(&EvidenceSummary{}, "evidence")
44+
}
45+
46+
func (es *EvidenceSummary) GetSummaryTitle() string {
47+
return "🔎 Evidence"
48+
}
49+
50+
func (es *EvidenceSummary) GenerateMarkdownFromFiles(dataFilePaths []string) (finalMarkdown string, err error) {
51+
log.Debug("Generating evidence summary markdown.")
52+
var evidenceData []EvidenceSummaryData
53+
for _, filePath := range dataFilePaths {
54+
var evidence EvidenceSummaryData
55+
if err = UnmarshalFromFilePath(filePath, &evidence); err != nil {
56+
log.Warn("Failed to unmarshal evidence data from file %s: %v", filePath, err)
57+
return
58+
}
59+
evidenceData = append(evidenceData, evidence)
60+
}
61+
62+
if len(evidenceData) == 0 {
63+
return
64+
}
65+
66+
tableMarkdown := es.generateEvidenceTable(evidenceData)
67+
return WrapCollapsableMarkdown(es.GetSummaryTitle(), tableMarkdown, evidenceHeaderSize), nil
68+
}
69+
70+
func (es *EvidenceSummary) generateEvidenceTable(evidenceData []EvidenceSummaryData) string {
71+
var tableBuilder strings.Builder
72+
tableBuilder.WriteString(es.getTableHeader())
73+
74+
for _, evidence := range evidenceData {
75+
es.appendEvidenceRow(&tableBuilder, evidence)
76+
}
77+
78+
tableBuilder.WriteString("</tbody></table> \n")
79+
return tableBuilder.String()
80+
}
81+
82+
func (es *EvidenceSummary) getTableHeader() string {
83+
return "<table><thead><tr><th>Evidence Subject</th><th>Evidence Type</th><th>Verification Status</th></tr></thead><tbody>\n"
84+
}
85+
86+
func (es *EvidenceSummary) appendEvidenceRow(tableBuilder *strings.Builder, evidence EvidenceSummaryData) {
87+
subject := es.formatSubjectWithLink(evidence)
88+
evidenceType := es.formatEvidenceType(evidence)
89+
verificationStatus := es.formatVerificationStatus(evidence.Verified)
90+
91+
tableBuilder.WriteString(fmt.Sprintf("<tr><td>%s</td><td>%s</td><td>%s</td></tr>\n", subject, evidenceType, verificationStatus))
92+
}
93+
94+
func (es *EvidenceSummary) formatSubjectWithLink(evidence EvidenceSummaryData) string {
95+
if evidence.Subject == "" {
96+
return "evidence"
97+
}
98+
99+
evidenceUrl, err := GenerateEvidenceUrlByType(evidence, evidenceSection)
100+
if err != nil {
101+
log.Warn("Failed to generate evidence URL: %v", err)
102+
evidenceUrl = ""
103+
}
104+
105+
displayName := evidence.DisplayName
106+
if displayName == "" {
107+
displayName = evidence.Subject
108+
}
109+
110+
var viewLink string
111+
subjectType := es.formatSubjectType(evidence.SubjectType)
112+
if evidenceUrl != "" {
113+
viewLink = fmt.Sprintf(`%s <a href="%s">%s</a>`, subjectType, evidenceUrl, displayName)
114+
} else {
115+
viewLink = fmt.Sprintf("%s %s", subjectType, displayName)
116+
}
117+
118+
return viewLink
119+
}
120+
121+
func (es *EvidenceSummary) formatVerificationStatus(verified bool) string {
122+
if verified {
123+
return fmt.Sprintf("%s Verified", "✅")
124+
}
125+
return fmt.Sprintf("%s Not Verified", "❌")
126+
}
127+
128+
func (es *EvidenceSummary) formatEvidenceType(evidence EvidenceSummaryData) string {
129+
if evidence.PredicateSlug == "" {
130+
if evidence.PredicateType == "" {
131+
return "⚠️ Unknown"
132+
}
133+
return evidence.PredicateType
134+
}
135+
return evidence.PredicateSlug
136+
}
137+
138+
func (es *EvidenceSummary) formatSubjectType(subjectType SubjectType) string {
139+
switch subjectType {
140+
case SubjectTypePackage:
141+
return "📦️"
142+
case SubjectTypeBuild:
143+
return "🛠️️"
144+
case SubjectTypeReleaseBundle:
145+
return "🧩"
146+
case SubjectTypeArtifact:
147+
return "📄"
148+
default:
149+
return ""
150+
}
151+
}

0 commit comments

Comments
 (0)