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

[MI-2339][server]: Added support to show pipeline build link preview. #104

Merged
merged 9 commits into from
Dec 14, 2022
10 changes: 10 additions & 0 deletions mocks/mock_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions server/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const (
// Regex to verify pull request link
PullRequestLinkRegex = `http(s)?:\/\/dev.azure.com\/[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]*\/[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]*\/_git\/[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]*\/pullrequest\/[1-9]+`

// Regex to verify pipeline build details link
BuildDetailsLinkRegex = `http(s)?:\/\/dev.azure.com\/[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]*\/[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]*\/_build\/results\?buildId=[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+`

// Azure API Versions
CreateTaskAPIVersion = "7.1-preview.3"
TasksIDAPIVersion = "5.1"
Expand Down Expand Up @@ -115,14 +118,16 @@ const (
WSEventSubscriptionDeleted = "subscription_deleted"

// Colors
ReposIconColor = "#d74f27"
BoardsIconColor = "#53bba1"
ReposIconColor = "#d74f27"
BoardsIconColor = "#53bba1"
PipelineIconColor = "#4275E4"
avas27JTG marked this conversation as resolved.
Show resolved Hide resolved

SubscriptionEventTypeDummy = "dummy"
FileNameGitBranchIcon = "git-branch-icon.svg"
FileNameProjectIcon = "project-icon.svg"
FileNameReposIcon = "repos-icon.svg"
FileNameBoardsIcon = "boards-icon.svg"
FileNamePipeline = "pipeline-icon.svg"
IconColorBoards = "#53bba1"
IconColorRepos = "#d74f27"

Expand Down
1 change: 1 addition & 0 deletions server/constants/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const (
CreatedTask = "[%s #%d](%s) (%s) was successfully created by %s."
TaskTitle = "[%s #%d: %s](%s)"
PullRequestTitle = "[#%d: %s](%s)"
BuildDetailsTitle = "[#%s](%s): %s"
AlreadyLinkedProject = "This project is already linked."
NoProjectLinked = "No project is linked, please link a project."

Expand Down
1 change: 1 addition & 0 deletions server/constants/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
CreateTask = "/%s/%s/_apis/wit/workitems/$%s?api-version=7.1-preview.3"
GetTask = "%s/%s/_apis/wit/workitems/%s?api-version=7.1-preview.3"
GetPullRequest = "%s/%s/_apis/git/pullrequests/%s?api-version=6.0"
GetBuildDetails = "%s/%s/_apis/build/builds/%s?api-version=6.0"
GetGitRepositories = "%s/%s/_apis/git/repositories?api-version=6.0"
GetGitRepositoryBranches = "%s/%s/_apis/git/repositories/%s/refs?filter=heads"
GetProject = "/%s/_apis/projects/%s?api-version=7.1-preview.4"
Expand Down
14 changes: 14 additions & 0 deletions server/plugin/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Client interface {
CheckIfUserIsProjectAdmin(organizationName, projectID, pluginURL, mattermostUserID string) (int, error)
GetGitRepositories(organization, projectName, mattermostUserID string) (*serializers.GitRepositoriesResponse, int, error)
GetGitRepositoryBranches(organization, projectName, repository, mattermostUserID string) (*serializers.GitBranchesResponse, int, error)
GetBuildDetails(organization, projectName, buildID, mattermostUserID string) (*serializers.BuildDetails, int, error)
}

type client struct {
Expand Down Expand Up @@ -116,6 +117,19 @@ func (c *client) GetPullRequest(organization, pullRequestID, projectName, matter
return pullRequest, statusCode, nil
}

// Function to get the pipeline build details.
func (c *client) GetBuildDetails(organization, projectName, buildID, mattermostUserID string) (*serializers.BuildDetails, int, error) {
buildDetailsURL := fmt.Sprintf(constants.GetBuildDetails, organization, projectName, buildID)

var buildDetails *serializers.BuildDetails
_, statusCode, err := c.CallJSON(c.plugin.getConfiguration().AzureDevopsAPIBaseURL, buildDetailsURL, http.MethodGet, mattermostUserID, nil, &buildDetails, nil)
if err != nil {
return nil, statusCode, errors.Wrap(err, "failed to get the pipeline build details")
}

return buildDetails, statusCode, nil
}

// Function to link a project and an organization.
func (c *client) Link(body *serializers.LinkRequestPayload, mattermostUserID string) (*serializers.Project, int, error) {
projectURL := fmt.Sprintf(constants.GetProject, body.Organization, body.Project)
Expand Down
6 changes: 6 additions & 0 deletions server/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,11 @@ func (p *Plugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*mode
return newPost, msg
}

// Check if a message contains a pipeline build link.
if buildDetailsData, link, isValid := IsLinkPresent(post.Message, constants.BuildDetailsLinkRegex); isValid {
newPost, msg := p.PostBuildDetailsPreview(buildDetailsData, link, post.UserId, post.ChannelId)
return newPost, msg
}

return nil, ""
}
48 changes: 48 additions & 0 deletions server/plugin/taskLinkPreview.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,51 @@ func (p *Plugin) PostPullRequestPreview(linkData []string, link, userID, channel

return post, ""
}

func (p *Plugin) PostBuildDetailsPreview(linkData []string, link, userID, channelID string) (*model.Post, string) {
organization := linkData[3]
project := linkData[4]
buildID := strings.Split(linkData[6], "&")[0][16:]
manojmalik20 marked this conversation as resolved.
Show resolved Hide resolved
buildDetails, _, err := p.Client.GetBuildDetails(organization, project, buildID, userID)
if err != nil {
return nil, ""
}
avas27JTG marked this conversation as resolved.
Show resolved Hide resolved

post := &model.Post{
UserId: userID,
ChannelId: channelID,
}
attachment := &model.SlackAttachment{
avas27JTG marked this conversation as resolved.
Show resolved Hide resolved
AuthorName: "Azure Pipeline",
avas27JTG marked this conversation as resolved.
Show resolved Hide resolved
AuthorIcon: fmt.Sprintf("%s/plugins/%s/static/%s", p.GetSiteURL(), constants.PluginID, constants.FileNamePipeline), // TODO: update icon file
manojmalik20 marked this conversation as resolved.
Show resolved Hide resolved
Title: fmt.Sprintf(constants.BuildDetailsTitle, buildDetails.BuildNumber, buildDetails.Link.Web.Href, buildDetails.Definition.Name),
Color: constants.PipelineIconColor,
Fields: []*model.SlackAttachmentField{
{
Title: "Repository",
Value: buildDetails.Repository.Name,
Short: true,
},
{
Title: "Source Branch",
Value: buildDetails.SourceBranch,
Short: true,
},
{
Title: "Requested By",
Value: buildDetails.RequestedBy.DisplayName,
Short: true,
},
{
Title: "Status",
Value: buildDetails.Status,
Short: true,
},
},
Footer: project,
FooterIcon: fmt.Sprintf("%s/plugins/%s/static/%s", p.GetSiteURL(), constants.PluginID, constants.FileNameProjectIcon),
avas27JTG marked this conversation as resolved.
Show resolved Hide resolved
}
model.ParseSlackAttachment(post, []*model.SlackAttachment{attachment})

return post, ""
avas27JTG marked this conversation as resolved.
Show resolved Hide resolved
}
19 changes: 19 additions & 0 deletions server/serializers/subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,25 @@ type DeleteSubscriptionRequestPayload struct {
Repository string `json:"repository"`
}

type BuildDetails struct {
BuildNumber string `json:"buildNumber"`
SourceBranch string `json:"sourceBranch"`
Repository Repository `json:"repository"`
Status string `json:"status"`
RequestedBy RequestedBy `json:"requestedBy"`
Project Project `json:"project"`
Link Link `json:"_links"`
Definition Definition `json:"definition"`
}

type RequestedBy struct {
DisplayName string `json:"displayName"`
}

type Definition struct {
Name string `json:"name"`
}

func CreateSubscriptionRequestPayloadFromJSON(data io.Reader) (*CreateSubscriptionRequestPayload, error) {
var body *CreateSubscriptionRequestPayload
if err := json.NewDecoder(data).Decode(&body); err != nil {
Expand Down
5 changes: 5 additions & 0 deletions server/serializers/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,17 @@ type TaskFieldValue struct {

type Link struct {
HTML Href `json:"html"`
Web Web `json:"web"`
}

type Href struct {
Href string `json:"href"`
}

type Web struct {
Href string `json:"href"`
}

avas27JTG marked this conversation as resolved.
Show resolved Hide resolved
type TaskUserDetails struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
Expand Down