Skip to content
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

(feat) internal/civisibility: add support for file environmental data #3319

Merged
merged 4 commits into from
Mar 27, 2025
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
3 changes: 3 additions & 0 deletions internal/civisibility/constants/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,7 @@ const (

// CIVisibilityAutoInstrumentationProviderEnvironmentVariable indicates that the auto-instrumentation script was used.
CIVisibilityAutoInstrumentationProviderEnvironmentVariable = "DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER"

// CIVisibilityEnvironmentDataFilePath is the environment variable that holds the path to the file containing the environmental data.
CIVisibilityEnvironmentDataFilePath = "DD_TEST_OPTIMIZATION_ENV_DATA_FILE"
)
36 changes: 27 additions & 9 deletions internal/civisibility/utils/codeowners.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,40 @@ func GetCodeOwners() *CodeOwners {
filepath.Join(v, ".docs", "CODEOWNERS"),
}
for _, path := range paths {
if _, err := os.Stat(path); err == nil {
codeowners, err = NewCodeOwners(path)
if err == nil {
if logger.DebugEnabled() {
logger.Debug("civisibility: codeowner file '%v' was loaded successfully.", path)
}
return codeowners
}
logger.Debug("Error parsing codeowners: %s", err)
if cow, err := parseCodeOwners(path); err == nil {
codeowners = cow
return codeowners
}
}
}

// If the codeowners file is not found, let's try a last resort by looking in the current directory (for standalone test binaries)
for _, path := range []string{"CODEOWNERS", filepath.Join(filepath.Dir(os.Args[0]), "CODEOWNERS")} {
if cow, err := parseCodeOwners(path); err == nil {
codeowners = cow
return codeowners
}
}

return nil
}

// parseCodeOwners reads and parses the CODEOWNERS file located at the given filePath.
func parseCodeOwners(filePath string) (*CodeOwners, error) {
if _, err := os.Stat(filePath); err != nil {
return nil, err
}
cow, err := NewCodeOwners(filePath)
if err == nil {
if logger.DebugEnabled() {
logger.Debug("civisibility: codeowner file '%v' was loaded successfully.", filePath)
}
return cow, nil
}
logger.Debug("Error parsing codeowners: %s", err)
return nil, err
}

// NewCodeOwners creates a new instance of CodeOwners by parsing a CODEOWNERS file located at the given filePath.
// It returns an error if the file cannot be read or parsed properly.
func NewCodeOwners(filePath string) (*CodeOwners, error) {
Expand Down
3 changes: 3 additions & 0 deletions internal/civisibility/utils/environmentTags.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ func createCITagsMap() map[string]string {
}
}

// Apply environmental data if is available
applyEnvironmentalDataIfRequired(localTags)

log.Debug("civisibility: workspace directory: %v", localTags[constants.CIWorkspacePath])
log.Debug("civisibility: common tags created with %v items", len(localTags))
return localTags
Expand Down
275 changes: 275 additions & 0 deletions internal/civisibility/utils/file_environmental_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2025 Datadog, Inc.

package utils

import (
"encoding/json"
"os"
"path/filepath"
"strings"
_ "unsafe" // required for go:linkname

"gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/constants"
logger "gopkg.in/DataDog/dd-trace-go.v1/internal/log"
)

type (
/*
{
"ci.workspace_path": "ci.workspace_path",
"git.repository_url": "git.repository_url",
"git.commit.sha": "git.commit.sha",
"git.branch": "user-supplied-branch",
"git.tag": "user-supplied-tag",
"git.commit.author.date": "usersupplied-authordate",
"git.commit.author.name": "usersupplied-authorname",
"git.commit.author.email": "usersupplied-authoremail",
"git.commit.committer.date": "usersupplied-comitterdate",
"git.commit.committer.name": "usersupplied-comittername",
"git.commit.committer.email": "usersupplied-comitteremail",
"git.commit.message": "usersupplied-message",
"ci.provider.name": "",
"ci.pipeline.id": "",
"ci.pipeline.url": "",
"ci.pipeline.name": "",
"ci.pipeline.number": "",
"ci.stage.name": "",
"ci.job.name": "",
"ci.job.url": "",
"ci.node.name": "",
"ci.node.labels": "",
"_dd.ci.env_vars": ""
}
*/

// fileEnvironmentalData represents the environmental data for the complete test session.
fileEnvironmentalData struct {
WorkspacePath string `json:"ci.workspace_path,omitempty"`
RepositoryURL string `json:"git.repository_url,omitempty"`
CommitSHA string `json:"git.commit.sha,omitempty"`
Branch string `json:"git.branch,omitempty"`
Tag string `json:"git.tag,omitempty"`
CommitAuthorDate string `json:"git.commit.author.date,omitempty"`
CommitAuthorName string `json:"git.commit.author.name,omitempty"`
CommitAuthorEmail string `json:"git.commit.author.email,omitempty"`
CommitCommitterDate string `json:"git.commit.committer.date,omitempty"`
CommitCommitterName string `json:"git.commit.committer.name,omitempty"`
CommitCommitterEmail string `json:"git.commit.committer.email,omitempty"`
CommitMessage string `json:"git.commit.message,omitempty"`
CIProviderName string `json:"ci.provider.name,omitempty"`
CIPipelineID string `json:"ci.pipeline.id,omitempty"`
CIPipelineURL string `json:"ci.pipeline.url,omitempty"`
CIPipelineName string `json:"ci.pipeline.name,omitempty"`
CIPipelineNumber string `json:"ci.pipeline.number,omitempty"`
CIStageName string `json:"ci.stage.name,omitempty"`
CIJobName string `json:"ci.job.name,omitempty"`
CIJobURL string `json:"ci.job.url,omitempty"`
CINodeName string `json:"ci.node.name,omitempty"`
CINodeLabels string `json:"ci.node.labels,omitempty"`
DDCIEnvVars string `json:"_dd.ci.env_vars,omitempty"`
}
)

// getEnvironmentalData reads the environmental data from the file.
//
//go:linkname getEnvironmentalData
func getEnvironmentalData() *fileEnvironmentalData {
envDataFileName := getEnvDataFileName()
if _, err := os.Stat(envDataFileName); os.IsNotExist(err) {
logger.Debug("civisibility: reading environmental data from %s not found.", envDataFileName)
return nil
}
file, err := os.Open(envDataFileName)
if err != nil {
logger.Error("civisibility: error reading environmental data from %s: %v", envDataFileName, err)
return nil
}
defer file.Close()
var envData fileEnvironmentalData
if err := json.NewDecoder(file).Decode(&envData); err != nil {
logger.Error("civisibility: error decoding environmental data from %s: %v", envDataFileName, err)
return nil
}
logger.Debug("civisibility: loaded environmental data from %s", envDataFileName)
return &envData
}

// getEnvDataFileName returns the environmental data file name.
//
//go:linkname getEnvDataFileName
func getEnvDataFileName() string {
envDataFileName := strings.TrimSpace(os.Getenv(constants.CIVisibilityEnvironmentDataFilePath))
if envDataFileName != "" {
return envDataFileName
}
cmd := filepath.Base(os.Args[0])
cmdWithoutExt := strings.TrimSuffix(cmd, filepath.Ext(cmd))
folder := filepath.Dir(os.Args[0])
return filepath.Join(folder, cmdWithoutExt+".env.json")
}

// applyEnvironmentalDataIfRequired applies the environmental data to the given tags if required.
//
//go:linkname applyEnvironmentalDataIfRequired
func applyEnvironmentalDataIfRequired(tags map[string]string) {
if tags == nil {
return
}
envData := getEnvironmentalData()
if envData == nil {
logger.Debug("civisibility: no environmental data found")
return
}

logger.Debug("civisibility: applying environmental data")

if envData.WorkspacePath != "" && tags[constants.CIWorkspacePath] == "" {
tags[constants.CIWorkspacePath] = envData.WorkspacePath
}

if envData.RepositoryURL != "" && tags[constants.GitRepositoryURL] == "" {
tags[constants.GitRepositoryURL] = envData.RepositoryURL
}

if envData.CommitSHA != "" && tags[constants.GitCommitSHA] == "" {
tags[constants.GitCommitSHA] = envData.CommitSHA
}

if envData.Branch != "" && tags[constants.GitBranch] == "" {
tags[constants.GitBranch] = envData.Branch
}

if envData.Tag != "" && tags[constants.GitTag] == "" {
tags[constants.GitTag] = envData.Tag
}

if envData.CommitAuthorDate != "" && tags[constants.GitCommitAuthorDate] == "" {
tags[constants.GitCommitAuthorDate] = envData.CommitAuthorDate
}

if envData.CommitAuthorName != "" && tags[constants.GitCommitAuthorName] == "" {
tags[constants.GitCommitAuthorName] = envData.CommitAuthorName
}

if envData.CommitAuthorEmail != "" && tags[constants.GitCommitAuthorEmail] == "" {
tags[constants.GitCommitAuthorEmail] = envData.CommitAuthorEmail
}

if envData.CommitCommitterDate != "" && tags[constants.GitCommitCommitterDate] == "" {
tags[constants.GitCommitCommitterDate] = envData.CommitCommitterDate
}

if envData.CommitCommitterName != "" && tags[constants.GitCommitCommitterName] == "" {
tags[constants.GitCommitCommitterName] = envData.CommitCommitterName
}

if envData.CommitCommitterEmail != "" && tags[constants.GitCommitCommitterEmail] == "" {
tags[constants.GitCommitCommitterEmail] = envData.CommitCommitterEmail
}

if envData.CommitMessage != "" && tags[constants.GitCommitMessage] == "" {
tags[constants.GitCommitMessage] = envData.CommitMessage
}

if envData.CIProviderName != "" && tags[constants.CIProviderName] == "" {
tags[constants.CIProviderName] = envData.CIProviderName
}

if envData.CIPipelineID != "" && tags[constants.CIPipelineID] == "" {
tags[constants.CIPipelineID] = envData.CIPipelineID
}

if envData.CIPipelineURL != "" && tags[constants.CIPipelineURL] == "" {
tags[constants.CIPipelineURL] = envData.CIPipelineURL
}

if envData.CIPipelineName != "" && tags[constants.CIPipelineName] == "" {
tags[constants.CIPipelineName] = envData.CIPipelineName
}

if envData.CIPipelineNumber != "" && tags[constants.CIPipelineNumber] == "" {
tags[constants.CIPipelineNumber] = envData.CIPipelineNumber
}

if envData.CIStageName != "" && tags[constants.CIStageName] == "" {
tags[constants.CIStageName] = envData.CIStageName
}

if envData.CIJobName != "" && tags[constants.CIJobName] == "" {
tags[constants.CIJobName] = envData.CIJobName
}

if envData.CIJobURL != "" && tags[constants.CIJobURL] == "" {
tags[constants.CIJobURL] = envData.CIJobURL
}

if envData.CINodeName != "" && tags[constants.CINodeName] == "" {
tags[constants.CINodeName] = envData.CINodeName
}

if envData.CINodeLabels != "" && tags[constants.CINodeLabels] == "" {
tags[constants.CINodeLabels] = envData.CINodeLabels
}

if envData.DDCIEnvVars != "" && tags[constants.CIEnvVars] == "" {
tags[constants.CIEnvVars] = envData.DDCIEnvVars
}
}

// createEnvironmentalDataFromTags creates a fileEnvironmentalData object from the given tags.
//
//go:linkname createEnvironmentalDataFromTags
func createEnvironmentalDataFromTags(tags map[string]string) *fileEnvironmentalData {
if tags == nil {
return nil
}

return &fileEnvironmentalData{
WorkspacePath: tags[constants.CIWorkspacePath],
RepositoryURL: tags[constants.GitRepositoryURL],
CommitSHA: tags[constants.GitCommitSHA],
Branch: tags[constants.GitBranch],
Tag: tags[constants.GitTag],
CommitAuthorDate: tags[constants.GitCommitAuthorDate],
CommitAuthorName: tags[constants.GitCommitAuthorName],
CommitAuthorEmail: tags[constants.GitCommitAuthorEmail],
CommitCommitterDate: tags[constants.GitCommitCommitterDate],
CommitCommitterName: tags[constants.GitCommitCommitterName],
CommitCommitterEmail: tags[constants.GitCommitCommitterEmail],
CommitMessage: tags[constants.GitCommitMessage],
CIProviderName: tags[constants.CIProviderName],
CIPipelineID: tags[constants.CIPipelineID],
CIPipelineURL: tags[constants.CIPipelineURL],
CIPipelineName: tags[constants.CIPipelineName],
CIPipelineNumber: tags[constants.CIPipelineNumber],
CIStageName: tags[constants.CIStageName],
CIJobName: tags[constants.CIJobName],
CIJobURL: tags[constants.CIJobURL],
CINodeName: tags[constants.CINodeName],
CINodeLabels: tags[constants.CINodeLabels],
DDCIEnvVars: tags[constants.CIEnvVars],
}
}

// writeEnvironmentalDataToFile writes the environmental data to a file.
//
//go:linkname writeEnvironmentalDataToFile
func writeEnvironmentalDataToFile(filePath string, tags map[string]string) error {
envData := createEnvironmentalDataFromTags(tags)
if envData == nil {
return nil
}

file, err := os.Create(filePath)
if err != nil {
return err
}
defer file.Close()

encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
return encoder.Encode(envData)
}
Loading
Loading