-
Notifications
You must be signed in to change notification settings - Fork 89
Add GitHub Actions summary for the create evidence command #1423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
| // 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 == "" { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the repokey in this case? |
||
| 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) | ||
| } | ||
| 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") | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } |
| 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 { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have to have it here? |
||
| 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"` | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 "" | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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.