mirror of
https://github.com/SAP/jenkins-library.git
synced 2025-02-21 19:48:53 +02:00
Include purl info in the event (#5092)
This commit is contained in:
parent
2175d3808b
commit
5230c3d454
@ -27,7 +27,6 @@ const (
|
|||||||
|
|
||||||
func mavenBuild(config mavenBuildOptions, telemetryData *telemetry.CustomData, commonPipelineEnvironment *mavenBuildCommonPipelineEnvironment) {
|
func mavenBuild(config mavenBuildOptions, telemetryData *telemetry.CustomData, commonPipelineEnvironment *mavenBuildCommonPipelineEnvironment) {
|
||||||
utils := maven.NewUtilsBundle()
|
utils := maven.NewUtilsBundle()
|
||||||
|
|
||||||
// enables url-log.json creation
|
// enables url-log.json creation
|
||||||
cmd := reflect.ValueOf(utils).Elem().FieldByName("Command")
|
cmd := reflect.ValueOf(utils).Elem().FieldByName("Command")
|
||||||
if cmd.IsValid() {
|
if cmd.IsValid() {
|
||||||
@ -62,7 +61,7 @@ func runMavenBuild(config *mavenBuildOptions, telemetryData *telemetry.CustomDat
|
|||||||
}
|
}
|
||||||
|
|
||||||
if config.CreateBOM {
|
if config.CreateBOM {
|
||||||
goals = append(goals, "org.cyclonedx:cyclonedx-maven-plugin:2.7.8:makeAggregateBom")
|
goals = append(goals, "org.cyclonedx:cyclonedx-maven-plugin:2.7.8:makeBom", "org.cyclonedx:cyclonedx-maven-plugin:2.7.8:makeAggregateBom")
|
||||||
createBOMConfig := []string{
|
createBOMConfig := []string{
|
||||||
"-DschemaVersion=1.4",
|
"-DschemaVersion=1.4",
|
||||||
"-DincludeBomSerialNumber=true",
|
"-DincludeBomSerialNumber=true",
|
||||||
@ -183,6 +182,7 @@ func runMavenBuild(config *mavenBuildOptions, telemetryData *telemetry.CustomDat
|
|||||||
} else {
|
} else {
|
||||||
coordinate.BuildPath = filepath.Dir(match)
|
coordinate.BuildPath = filepath.Dir(match)
|
||||||
coordinate.URL = config.AltDeploymentRepositoryURL
|
coordinate.URL = config.AltDeploymentRepositoryURL
|
||||||
|
coordinate.PURL = getPurlForThePomAndDeleteIndividualBom(match)
|
||||||
buildCoordinates = append(buildCoordinates, coordinate)
|
buildCoordinates = append(buildCoordinates, coordinate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -209,6 +209,42 @@ func runMavenBuild(config *mavenBuildOptions, telemetryData *telemetry.CustomDat
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getPurlForThePomAndDeleteIndividualBom(pomFilePath string) string {
|
||||||
|
bomPath := filepath.Join(filepath.Dir(pomFilePath) + "/target/" + mvnBomFilename + ".xml")
|
||||||
|
exists, _ := piperutils.FileExists(bomPath)
|
||||||
|
if !exists {
|
||||||
|
log.Entry().Debugf("bom file doesn't exist and hence no pURL info: %v", bomPath)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
bom, err := piperutils.GetBom(bomPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Entry().Warnf("failed to get bom file %s: %v", bomPath, err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Entry().Debugf("Found purl: %s for the bomPath: %s", bom.Metadata.Component.Purl, bomPath)
|
||||||
|
purl := bom.Metadata.Component.Purl
|
||||||
|
|
||||||
|
// Check if the BOM is an aggregated BOM
|
||||||
|
if !isAggregatedBOM(bom) {
|
||||||
|
// Delete the individual BOM file
|
||||||
|
err = os.Remove(bomPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Entry().Warnf("failed to delete bom file %s: %v", bomPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return purl
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAggregatedBOM(bom piperutils.Bom) bool {
|
||||||
|
for _, property := range bom.Metadata.Properties {
|
||||||
|
if property.Name == "maven.goal" && property.Value == "makeAggregateBom" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func createOrUpdateProjectSettingsXML(projectSettingsFile string, altDeploymentRepositoryID string, altDeploymentRepositoryUser string, altDeploymentRepositoryPassword string, utils maven.Utils) (string, error) {
|
func createOrUpdateProjectSettingsXML(projectSettingsFile string, altDeploymentRepositoryID string, altDeploymentRepositoryUser string, altDeploymentRepositoryPassword string, utils maven.Utils) (string, error) {
|
||||||
if len(projectSettingsFile) > 0 {
|
if len(projectSettingsFile) > 0 {
|
||||||
projectSettingsFilePath, err := maven.UpdateProjectSettingsXML(projectSettingsFile, altDeploymentRepositoryID, altDeploymentRepositoryUser, altDeploymentRepositoryPassword, utils)
|
projectSettingsFilePath, err := maven.UpdateProjectSettingsXML(projectSettingsFile, altDeploymentRepositoryID, altDeploymentRepositoryUser, altDeploymentRepositoryPassword, utils)
|
||||||
|
@ -4,8 +4,11 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/SAP/jenkins-library/pkg/piperutils"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -157,3 +160,105 @@ func TestMavenBuild(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsAggregatedBOM(t *testing.T) {
|
||||||
|
t.Run("is aggregated BOM", func(t *testing.T) {
|
||||||
|
bom := piperutils.Bom{
|
||||||
|
Metadata: piperutils.Metadata{
|
||||||
|
Properties: []piperutils.BomProperty{
|
||||||
|
{Name: "maven.goal", Value: "makeAggregateBom"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.True(t, isAggregatedBOM(bom))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("is not aggregated BOM", func(t *testing.T) {
|
||||||
|
bom := piperutils.Bom{
|
||||||
|
Metadata: piperutils.Metadata{
|
||||||
|
Properties: []piperutils.BomProperty{
|
||||||
|
{Name: "some.property", Value: "someValue"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.False(t, isAggregatedBOM(bom))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTempFile(t *testing.T, dir string, filename string, content string) string {
|
||||||
|
filePath := filepath.Join(dir, filename)
|
||||||
|
err := os.WriteFile(filePath, []byte(content), 0666)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp file: %s", err)
|
||||||
|
}
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPurlForThePomAndDeleteIndividualBom(t *testing.T) {
|
||||||
|
t.Run("valid BOM file, non-aggregated", func(t *testing.T) {
|
||||||
|
tempDir, err := piperutils.Files{}.TempDir("", "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bomContent := `<bom>
|
||||||
|
<metadata>
|
||||||
|
<component>
|
||||||
|
<purl>pkg:maven/com.example/mycomponent@1.0.0</purl>
|
||||||
|
</component>
|
||||||
|
<properties>
|
||||||
|
<property name="name1" value="value1" />
|
||||||
|
</properties>
|
||||||
|
</metadata>
|
||||||
|
</bom>`
|
||||||
|
pomFilePath := createTempFile(t, tempDir, "pom.xml", "")
|
||||||
|
bomDir := filepath.Join(tempDir, "target")
|
||||||
|
if err := os.MkdirAll(bomDir, 0777); err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %s", err)
|
||||||
|
}
|
||||||
|
bomFilePath := createTempFile(t, bomDir, mvnBomFilename+".xml", bomContent)
|
||||||
|
defer os.Remove(bomFilePath)
|
||||||
|
|
||||||
|
purl := getPurlForThePomAndDeleteIndividualBom(pomFilePath)
|
||||||
|
assert.Equal(t, "pkg:maven/com.example/mycomponent@1.0.0", purl)
|
||||||
|
_, err = os.Stat(bomFilePath)
|
||||||
|
assert.True(t, os.IsNotExist(err))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid BOM file, aggregated BOM", func(t *testing.T) {
|
||||||
|
tempDir, err := piperutils.Files{}.TempDir("", "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bomContent := `<bom>
|
||||||
|
<metadata>
|
||||||
|
<component>
|
||||||
|
<purl>pkg:maven/com.example/aggregatecomponent@1.0.0</purl>
|
||||||
|
</component>
|
||||||
|
<properties>
|
||||||
|
<property name="maven.goal" value="makeAggregateBom" />
|
||||||
|
</properties>
|
||||||
|
</metadata>
|
||||||
|
</bom>`
|
||||||
|
pomFilePath := createTempFile(t, tempDir, "pom.xml", "")
|
||||||
|
bomDir := filepath.Join(tempDir, "target")
|
||||||
|
if err := os.MkdirAll(bomDir, 0777); err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %s", err)
|
||||||
|
}
|
||||||
|
bomFilePath := createTempFile(t, bomDir, mvnBomFilename+".xml", bomContent)
|
||||||
|
|
||||||
|
purl := getPurlForThePomAndDeleteIndividualBom(pomFilePath)
|
||||||
|
assert.Equal(t, "pkg:maven/com.example/aggregatecomponent@1.0.0", purl)
|
||||||
|
_, err = os.Stat(bomFilePath)
|
||||||
|
assert.False(t, os.IsNotExist(err)) // File should not be deleted
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("BOM file does not exist", func(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
pomFilePath := createTempFile(t, tempDir, "pom.xml", "") // Create a temp pom file
|
||||||
|
|
||||||
|
purl := getPurlForThePomAndDeleteIndividualBom(pomFilePath)
|
||||||
|
assert.Equal(t, "", purl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -217,6 +217,7 @@ func (exec *Execute) publish(packageJSON, registry, username, password string, p
|
|||||||
coordinate.BuildPath = filepath.Dir(packageJSON)
|
coordinate.BuildPath = filepath.Dir(packageJSON)
|
||||||
coordinate.URL = registry
|
coordinate.URL = registry
|
||||||
coordinate.Packaging = "tgz"
|
coordinate.Packaging = "tgz"
|
||||||
|
coordinate.PURL = getPurl(packageJSON)
|
||||||
|
|
||||||
*buildCoordinates = append(*buildCoordinates, coordinate)
|
*buildCoordinates = append(*buildCoordinates, coordinate)
|
||||||
}
|
}
|
||||||
@ -225,6 +226,21 @@ func (exec *Execute) publish(packageJSON, registry, username, password string, p
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getPurl(packageJSON string) string {
|
||||||
|
expectedBomFilePath := filepath.Join(filepath.Dir(packageJSON) + "/" + npmBomFilename)
|
||||||
|
exists, _ := CredentialUtils.FileExists(expectedBomFilePath)
|
||||||
|
if !exists {
|
||||||
|
log.Entry().Debugf("bom file doesn't exist and hence no pURL info: %v", expectedBomFilePath)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
bom, err := CredentialUtils.GetBom(expectedBomFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Entry().Warnf("unable to get bom metdata : %v", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return bom.Metadata.Component.Purl
|
||||||
|
}
|
||||||
|
|
||||||
func (exec *Execute) readPackageScope(packageJSON string) (string, error) {
|
func (exec *Execute) readPackageScope(packageJSON string) (string, error) {
|
||||||
b, err := exec.Utils.FileRead(packageJSON)
|
b, err := exec.Utils.FileRead(packageJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/SAP/jenkins-library/pkg/piperutils"
|
"github.com/SAP/jenkins-library/pkg/piperutils"
|
||||||
"github.com/SAP/jenkins-library/pkg/versioning"
|
"github.com/SAP/jenkins-library/pkg/versioning"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
type npmMockUtilsBundleRelativeGlob struct {
|
type npmMockUtilsBundleRelativeGlob struct {
|
||||||
@ -573,3 +574,46 @@ func TestNpmPublish(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createTempFile(t *testing.T, dir string, filename string, content string) string {
|
||||||
|
filePath := filepath.Join(dir, filename)
|
||||||
|
err := os.WriteFile(filePath, []byte(content), 0666)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp file: %s", err)
|
||||||
|
}
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPurl(t *testing.T) {
|
||||||
|
t.Run("valid BOM file", func(t *testing.T) {
|
||||||
|
tempDir, err := piperutils.Files{}.TempDir("", "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bomContent := `<bom>
|
||||||
|
<metadata>
|
||||||
|
<component>
|
||||||
|
<purl>pkg:npm/com.example/mycomponent@1.0.0</purl>
|
||||||
|
</component>
|
||||||
|
<properties>
|
||||||
|
<property name="name1" value="value1" />
|
||||||
|
</properties>
|
||||||
|
</metadata>
|
||||||
|
</bom>`
|
||||||
|
packageJsonFilePath := createTempFile(t, tempDir, "package.json", "")
|
||||||
|
bomFilePath := createTempFile(t, tempDir, npmBomFilename, bomContent)
|
||||||
|
defer os.Remove(bomFilePath)
|
||||||
|
|
||||||
|
purl := getPurl(packageJsonFilePath)
|
||||||
|
assert.Equal(t, "pkg:npm/com.example/mycomponent@1.0.0", purl)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("BOM file does not exist", func(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
packageJsonFilePath := createTempFile(t, tempDir, "pom.xml", "") // Create a temp pom file
|
||||||
|
|
||||||
|
purl := getPurl(packageJsonFilePath)
|
||||||
|
assert.Equal(t, "", purl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
48
pkg/piperutils/cyclonedxBom.go
Normal file
48
pkg/piperutils/cyclonedxBom.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package piperutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"github.com/SAP/jenkins-library/pkg/log"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// To serialize the cyclonedx BOM file
|
||||||
|
type Bom struct {
|
||||||
|
Metadata Metadata `xml:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Metadata struct {
|
||||||
|
Component BomComponent `xml:"component"`
|
||||||
|
Properties []BomProperty `xml:"properties>property"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BomProperty struct {
|
||||||
|
Name string `xml:"name,attr"`
|
||||||
|
Value string `xml:"value,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BomComponent struct {
|
||||||
|
Purl string `xml:"purl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBom(absoluteBomPath string) (Bom, error) {
|
||||||
|
xmlFile, err := os.Open(absoluteBomPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Entry().Debugf("failed to open bom file %s", absoluteBomPath)
|
||||||
|
return Bom{}, err
|
||||||
|
}
|
||||||
|
defer xmlFile.Close()
|
||||||
|
byteValue, err := io.ReadAll(xmlFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Entry().Debugf("failed to read bom file %s", absoluteBomPath)
|
||||||
|
return Bom{}, err
|
||||||
|
}
|
||||||
|
var bom Bom
|
||||||
|
err = xml.Unmarshal(byteValue, &bom)
|
||||||
|
if err != nil {
|
||||||
|
log.Entry().Debugf("failed to unmarshal bom file %s", absoluteBomPath)
|
||||||
|
return Bom{}, err
|
||||||
|
}
|
||||||
|
return bom, nil
|
||||||
|
}
|
126
pkg/piperutils/cyclonedxbom_test.go
Normal file
126
pkg/piperutils/cyclonedxbom_test.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
package piperutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createTempFile(t *testing.T, content string) (string, func()) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
fileName := filepath.Join(dir, "test.xml")
|
||||||
|
err := os.WriteFile(fileName, []byte(content), 0666)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp file: %s", err)
|
||||||
|
}
|
||||||
|
return fileName, func() {
|
||||||
|
os.Remove(fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetBom(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
xmlContent string
|
||||||
|
expectedBom Bom
|
||||||
|
expectError bool
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid file",
|
||||||
|
xmlContent: `<bom>
|
||||||
|
<metadata>
|
||||||
|
<component>
|
||||||
|
<purl>pkg:maven/com.example/mycomponent@1.0.0</purl>
|
||||||
|
</component>
|
||||||
|
<properties>
|
||||||
|
<property name="name1" value="value1" />
|
||||||
|
<property name="name2" value="value2" />
|
||||||
|
</properties>
|
||||||
|
</metadata>
|
||||||
|
</bom>`,
|
||||||
|
expectedBom: Bom{
|
||||||
|
Metadata: Metadata{
|
||||||
|
Component: BomComponent{
|
||||||
|
Purl: "pkg:maven/com.example/mycomponent@1.0.0",
|
||||||
|
},
|
||||||
|
Properties: []BomProperty{
|
||||||
|
{Name: "name1", Value: "value1"},
|
||||||
|
{Name: "name2", Value: "value2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file not found",
|
||||||
|
xmlContent: "",
|
||||||
|
expectedBom: Bom{},
|
||||||
|
expectError: true,
|
||||||
|
expectedError: "no such file or directory",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid XML file",
|
||||||
|
xmlContent: "<bom><metadata><component><purl>invalid xml</metadata></bom>",
|
||||||
|
expectedBom: Bom{},
|
||||||
|
expectError: true,
|
||||||
|
expectedError: "XML syntax error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var fileName string
|
||||||
|
var cleanup func()
|
||||||
|
if tt.xmlContent != "" {
|
||||||
|
var err error
|
||||||
|
fileName, cleanup = createTempFile(t, tt.xmlContent)
|
||||||
|
defer cleanup()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp file: %s", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use a non-existent file path
|
||||||
|
fileName = "nonexistent.xml"
|
||||||
|
}
|
||||||
|
|
||||||
|
bom, err := GetBom(fileName)
|
||||||
|
if (err != nil) != tt.expectError {
|
||||||
|
t.Errorf("Expected error: %v, got: %v", tt.expectError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && !tt.expectError {
|
||||||
|
if !tt.expectError && !containsSubstring(err.Error(), tt.expectedError) {
|
||||||
|
t.Errorf("Expected error message: %v, got: %v", tt.expectedError, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tt.expectError && !bomEquals(bom, tt.expectedBom) {
|
||||||
|
t.Errorf("Expected BOM: %+v, got: %+v", tt.expectedBom, bom)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bomEquals(a, b Bom) bool {
|
||||||
|
// compare a and b manually since reflect.DeepEqual can be problematic with slices and nil values
|
||||||
|
return a.Metadata.Component.Purl == b.Metadata.Component.Purl &&
|
||||||
|
len(a.Metadata.Properties) == len(b.Metadata.Properties) &&
|
||||||
|
propertiesMatch(a.Metadata.Properties, b.Metadata.Properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
func propertiesMatch(a, b []BomProperty) bool {
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsSubstring(str, substr string) bool {
|
||||||
|
if len(substr) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return len(str) >= len(substr) && str[:len(substr)] == substr
|
||||||
|
}
|
@ -19,6 +19,7 @@ type Coordinates struct {
|
|||||||
Packaging string
|
Packaging string
|
||||||
BuildPath string
|
BuildPath string
|
||||||
URL string
|
URL string
|
||||||
|
PURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Artifact defines the versioning operations for various build tools
|
// Artifact defines the versioning operations for various build tools
|
||||||
|
Loading…
x
Reference in New Issue
Block a user