diff --git a/plugin/plugin.go b/plugin/plugin.go index 7e0d534..bdf3bea 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -45,6 +45,10 @@ type Args struct { // Deployment environment (optional) EnvironmentName string `envconfig:"PLUGIN_ENVIRONMENT_NAME"` + // Environmnet Id (optional) + EnvironmentId string `envconfig:"PLUGIN_ENVIRONMENT_ID"` + // Environmnet Type (optional) + EnvironmentType string `envconfig:"PLUGIN_ENVIRONMENT_TYPE"` // Link to deployment (optional) Link string `envconfig:"PLUGIN_LINK"` @@ -67,51 +71,66 @@ type Args struct { // connect hostname (required) ConnectHostname string `envconfig:"PLUGIN_CONNECT_HOSTNAME"` + // Issue Keys(optional) + IssueKeys []string `envconfig:"PLUGIN_ISSUEKEYS"` } // Exec executes the plugin. func Exec(ctx context.Context, args Args) error { var ( - environ = toEnvironment(args) - issue = extractIssue(args) - state = toState(args) - version = toVersion(args) - deeplink = toLink(args) + environ = toEnvironment(args) + environmentID = toEnvironmentId(args) + environmentType = toEnvironmentType(args) + issues []string + state = toState(args) + version = toVersion(args) + deeplink = toLink(args) ) + // ExtractInstanceName extracts the instance name from the provided URL if any + instanceName := ExtractInstanceName(args.Instance) + logger := logrus. WithField("client_id", args.ClientID). WithField("cloud_id", args.CloudID). WithField("project_id", args.Project). - WithField("instance", args.Instance). + WithField("instance", instanceName). WithField("pipeline", args.Name). WithField("environment", environ). WithField("state", state). - WithField("version", version) + WithField("environment Type", environmentType). + WithField("environment ID", environmentID) - if issue == "" { - logger.Debugln("cannot find issue number") - return errors.New("failed to extract issue number") + //check if PLUGIN_ISSUEKEYS is provided + if len(args.IssueKeys) > 0 { + issues = args.IssueKeys + } else { + // fallback to extracting from commit if no issue keys are passed + var issue string = extractIssue(args) + if issue == "" { + logger.Debugln("cannot find issue number") + return errors.New("failed to extract issue number") + } + issues = []string{issue} // add the single issue here for consistency } commitMessage := args.Commit.Message - if len(commitMessage) > 255 { - logger.Warnln("Commit message exceeds 255 characters; truncating to fit.") - commitMessage = commitMessage[:252] + "..." - } + if len(commitMessage) > 255 { + logger.Warnln("Commit message exceeds 255 characters; truncating to fit.") + commitMessage = commitMessage[:252] + "..." + } - logger = logger.WithField("issue", issue) logger.Debugln("successfully extraced issue number") - deploymentPayload := DeploymentPayload{ Deployments: []*Deployment{ { Deploymentsequencenumber: args.Build.Number, Updatesequencenumber: args.Build.Number, + IssueKeys: issues, Associations: []Association{ { Associationtype: "issueIdOrKeys", - Values: []string{issue}, + Values: issues, }, }, Displayname: strconv.Itoa(args.Build.Number), @@ -125,13 +144,42 @@ func Exec(ctx context.Context, args Args) error { URL: deeplink, }, Environment: Environment{ - ID: environ, + ID: environmentID, Displayname: environ, - Type: environ, + Type: environmentType, }, }, }, } + if len(args.IssueKeys) > 0 { + deploymentPayload.Deployments[0].Associations = nil + } + // Initialize an empty reference + references := []Reference{} + // Check if any input is available to update the reference + if args.Commit.Branch != "" || args.Commit.Link != "" || args.Commit.Rev != "" { + var reference Reference + // Update CommitInfo if Rev or Link is provided + if args.Commit.Rev != "" || args.Commit.Link != "" { + reference.Commit = &CommitInfo{ + ID: args.Commit.Rev, + RepositoryURI: args.Commit.Link, + } + } + // Update RefInfo if both Branch and Link are provided + if args.Commit.Branch != "" && args.Commit.Link != "" { + reference.Ref = &RefInfo{ + Name: args.Commit.Branch, + URI: fmt.Sprintf("%s/refs/%s", args.Commit.Link, args.Commit.Branch), + } + } + + // Append the reference if at least one field is populated + if reference.Commit != nil || reference.Ref != nil { + references = append(references, reference) + } + } + // Build the Build struct and include references only if non-empty buildPayload := BuildPayload{ Builds: []*Build{ { @@ -141,13 +189,13 @@ func Exec(ctx context.Context, args Args) error { URL: deeplink, LastUpdated: time.Now(), PipelineID: args.Name, - IssueKeys: []string{issue}, + IssueKeys: issues, State: state, UpdateSequenceNumber: args.Build.Number, + References: references, }, }, } - // validation of arguments if (args.ClientID == "" && args.ClientSecret == "") && (args.ConnnectKey == "") { logger.Debugln("client id and secret are empty. specify the client id and secret or specify connect key") @@ -156,7 +204,7 @@ func Exec(ctx context.Context, args Args) error { // create tokens and deployments if args.ClientID != "" && args.ClientSecret != "" { // get cloud id - cloudID, err := getCloudID(args.Instance, args.CloudID) + cloudID, err := getCloudID(instanceName, args.CloudID) if err != nil { logger.Debugln("cannot get cloud id") return err @@ -187,7 +235,7 @@ func Exec(ctx context.Context, args Args) error { } if args.EnvironmentName != "" { logger.Infoln("creating deployment") - deploymentErr := createConnectDeployment(deploymentPayload, args.Instance, args.Level, jwtToken) + deploymentErr := createConnectDeployment(deploymentPayload, instanceName, args.Level, jwtToken) if deploymentErr != nil { logger.WithError(deploymentErr). Errorln("cannot create deployment") @@ -195,7 +243,7 @@ func Exec(ctx context.Context, args Args) error { } } else { logger.Infoln("creating build") - buildErr := createConnectBuild(buildPayload, args.Instance, args.Level, jwtToken) + buildErr := createConnectBuild(buildPayload, instanceName, args.Level, jwtToken) if buildErr != nil { logger.WithError(buildErr). Errorln("cannot create build") @@ -204,15 +252,23 @@ func Exec(ctx context.Context, args Args) error { } } // only create card if the state is successful - ticketLink := fmt.Sprintf("https://%s.atlassian.net/browse/%s", args.Instance, issue) + + var ticketLinks []string + + if len(issues) > 0 { + for _, issue_key := range issues { + ticketLink := fmt.Sprintf("https://%s.atlassian.net/browse/%s", args.Instance, issue_key) + ticketLinks = append(ticketLinks, ticketLink) + } + } cardData := Card{ Pipeline: args.Name, - Instance: args.Instance, + Instance: instanceName, Project: args.Project, State: state, Version: version, Environment: environ, - URL: ticketLink, + URL: ticketLinks, } if err := args.writeCard(cardData); err != nil { fmt.Printf("Could not create adaptive card. %s\n", err) @@ -257,6 +313,7 @@ func getOauthToken(args Args) (string, error) { if err != nil { return "", err } + // fmt.Println(output["access_token"].(string)) return output["access_token"].(string), nil } diff --git a/plugin/type.go b/plugin/type.go index 9aaac27..68ebcd8 100644 --- a/plugin/type.go +++ b/plugin/type.go @@ -16,25 +16,16 @@ type ( // build provides the build details. Build struct { - BuildNumber int `json:"buildNumber"` - Description string `json:"description"` - DisplayName string `json:"displayName"` - IssueKeys []string `json:"issueKeys"` - Label string `json:"label"` - LastUpdated time.Time `json:"lastUpdated"` - PipelineID string `json:"pipelineId"` - References []struct { - Commit struct { - ID string `json:"id"` - RepositoryURI string `json:"repositoryUri"` - } `json:"commit"` - Ref struct { - Name string `json:"name"` - URI string `json:"uri"` - } `json:"ref"` - } `json:"references"` - SchemaVersion string `json:"schemaVersion"` - State string `json:"state"` + BuildNumber int `json:"buildNumber"` + Description string `json:"description"` + DisplayName string `json:"displayName"` + IssueKeys []string `json:"issueKeys"` + Label string `json:"label"` + LastUpdated time.Time `json:"lastUpdated"` + PipelineID string `json:"pipelineId"` + References []Reference `json:"references,omitempty"` + SchemaVersion string `json:"schemaVersion"` + State string `json:"state"` TestInfo struct { NumberFailed int64 `json:"numberFailed"` NumberPassed int64 `json:"numberPassed"` @@ -44,25 +35,39 @@ type ( UpdateSequenceNumber int `json:"updateSequenceNumber"` URL string `json:"url"` } + Reference struct { + Commit *CommitInfo `json:"commit,omitempty"` // Use a pointer to omit if nil + Ref *RefInfo `json:"ref,omitempty"` // Use a pointer to omit if nil + } + CommitInfo struct { + ID string `json:"id,omitempty"` + RepositoryURI string `json:"repositoryUri,omitempty"` + } + RefInfo struct { + Name string `json:"name,omitempty"` + URI string `json:"uri,omitempty"` + } // Deployment provides the Deployment details. Deployment struct { - Deploymentsequencenumber int `json:"deploymentSequenceNumber"` - Updatesequencenumber int `json:"updateSequenceNumber"` - Associations []Association `json:"associations"` - Displayname string `json:"displayName"` - URL string `json:"url"` - Description string `json:"description"` - Lastupdated time.Time `json:"lastUpdated"` - State string `json:"state"` - Pipeline JiraPipeline `json:"pipeline"` - Environment Environment `json:"environment"` + Deploymentsequencenumber int `json:"deploymentSequenceNumber"` + //IssueKeys []string `json:"issueKeys"` + IssueKeys []string `json:"issueKeys,omitempty"` + Updatesequencenumber int `json:"updateSequenceNumber"` + Associations []Association `json:"associations"` + Displayname string `json:"displayName"` + URL string `json:"url"` + Description string `json:"description"` + Lastupdated time.Time `json:"lastUpdated"` + State string `json:"state"` + Pipeline JiraPipeline `json:"pipeline"` + Environment Environment `json:"environment"` } // Association provides the association details. Association struct { - Associationtype string `json:"associationType"` - Values []string `json:"values"` + Associationtype string `json:"associationType,omitempty"` + Values []string `json:"values,omitempty"` } // Environment provides the environment details. @@ -86,12 +91,12 @@ type ( // struct for adaptive card Card struct { - Pipeline string `json:"pipeline"` - Instance string `json:"instance"` - Project string `json:"project"` - State string `json:"state"` - Version string `json:"version"` - Environment string `json:"environment"` - URL string `json:"url"` + Pipeline string `json:"pipeline"` + Instance string `json:"instance"` + Project string `json:"project"` + State string `json:"state"` + Version string `json:"version"` + Environment string `json:"environment"` + URL []string `json:"url"` } ) diff --git a/plugin/util.go b/plugin/util.go index 71f7a5b..56c976f 100644 --- a/plugin/util.go +++ b/plugin/util.go @@ -6,8 +6,11 @@ package plugin import ( "fmt" + "net/url" "regexp" "strings" + + "github.com/sirupsen/logrus" ) // helper function to extract the issue number from @@ -33,7 +36,7 @@ func toState(args Args) string { return toStateEnum(args.Build.Status) } -// helper function determines the target environment. +// helper function determines the target environment Name. func toEnvironment(args Args) string { if v := args.EnvironmentName; v != "" { return toEnvironmentEnum(v) @@ -45,6 +48,24 @@ func toEnvironment(args Args) string { return "production" } +// helper function determines the target environment Id. +func toEnvironmentId(args Args) string { + if v := args.EnvironmentId; v != "" { + return v + } + // Return a default value, such as an empty string + return "" +} + +// helper function determines the target environment Type. +func toEnvironmentType(args Args) string { + if v := args.EnvironmentType; v != "" { + return v + } + // Return a default value, such as an empty string + return "" +} + // helper function determines the version number. func toVersion(args Args) string { if v := args.Semver.Version; v != "" { @@ -68,6 +89,33 @@ func toLink(args Args) string { return args.Commit.Link } +// helper function ExtractInstanceName extracts the instance name from the provided URL +// or returns the instance name directly +func ExtractInstanceName(instance string) string { + // Check if the instance is a full URL + if strings.Contains(instance, "://") { + parsedURL, err := url.Parse(instance) + if err == nil { + // Return the host part without the top-level domain + hostParts := strings.Split(parsedURL.Hostname(), ".") + if len(hostParts) > 0 { + return hostParts[0] // Return the first part as the instance name + } + } else { + // Log the error if URL parsing fails + logrus.WithField("instance", instance).WithField("err", err).Error("Error parsing URL") + } + } else { + // If it's not a URL, split by dots to get the instance name + hostParts := strings.Split(instance, ".") + if len(hostParts) > 0 { + return hostParts[0] // Return the first part as the instance name + } + } + // Default return if no valid instance name is found + return instance +} + // helper function normalizes the environment to match // the expected bitbucket enum. func toEnvironmentEnum(s string) string { diff --git a/plugin/util_test.go b/plugin/util_test.go index a270136..1ce32c4 100644 --- a/plugin/util_test.go +++ b/plugin/util_test.go @@ -45,3 +45,89 @@ func TestExtractIssue(t *testing.T) { } } } + +func TestExtractInstanceName(t *testing.T) { + tests := []struct { + text string + want string + }{ + // Test cases with URLs + {"http://test.com", "test"}, + {"https://subdomain.test.com", "subdomain"}, + {"ftp://ftp.test.org", "ftp"}, + + // Test cases with non-URL strings + {"instance.test.com", "instance"}, + {"subdomain.instance.test.org", "subdomain"}, + {"localhost", "localhost"}, + + // Test invalid or malformed URLs + {"http://", ""}, // Invalid URL with no hostname + {"invalid-url", "invalid-url"}, // Not a URL, should return the input string + } + + for _, test := range tests { + result := ExtractInstanceName(test.text) + if result != test.want { + t.Errorf("ExtractInstanceName(%q) = %q; expected %q", test.text, result, test.want) + } + } +} + +// Test the toEnvironmentId function +func TestToEnvironmentId(t *testing.T) { + tests := []struct { + name string + args Args + expectedOutput string + }{ + { + name: "Non-empty EnvironmentId", + args: Args{EnvironmentId: "env-123"}, + expectedOutput: "env-123", + }, + { + name: "Empty EnvironmentId", + args: Args{EnvironmentId: ""}, + expectedOutput: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := toEnvironmentId(tt.args) + if result != tt.expectedOutput { + t.Errorf("toEnvironmentId() = %v, want %v", result, tt.expectedOutput) + } + }) + } +} + +// Test the toEnvironmentType function +func TestToEnvironmentType(t *testing.T) { + tests := []struct { + name string + args Args + expectedOutput string + }{ + { + name: "Non-empty EnvironmentType", + args: Args{EnvironmentType: "prod"}, + expectedOutput: "prod", + }, + { + name: "Empty EnvironmentType", + args: Args{EnvironmentType: ""}, + expectedOutput: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := toEnvironmentType(tt.args) + if result != tt.expectedOutput { + t.Errorf("toEnvironmentType() = %v, want %v", result, tt.expectedOutput) + } + }) + } +}