DEV Community

Cover image for Custom GitLab Pipeline Notifications To Microsoft Teams
maksimmuravev
maksimmuravev

Posted on

Custom GitLab Pipeline Notifications To Microsoft Teams

Have you ever set up notifications from your loved CI/CD system to messenger apps? I received such a request recently, but I didn't take it seriously because most build systems these days have great integration with widespread messaging apps, especially Microsoft Teams.

I was in this nonchalant position until I read the developer's request thoroughly. They said:

Need to set up notifications (only tags notification, for only mentioned projects) to Teams channels about build results on Gitlab.

In other words, they wanted to receive notifications only for pipelines triggered by Git tags.

Well.

Immediately, I navigated to the configuration page in GitLab and selected a random project's integration section. Lo and behold, Microsoft Teams was available 😊.

Image description

Regarding the specific pipeline results the developer requested, GitLab offered an option for when A pipeline status changes. However, this could have been better because it would send notifications for all pipelines, regardless of whether they were triggered by tags, feature branches, or the mainline.

On the other hand, there is an option for when A tag is pushed to the repository or removed. However, this also doesn't suit our needs because it only sends notifications for actual Git pushes rather than the build pipeline situation.

I'll need to cook up a different approach to tackle this challenge. And to make this dish (spoiler alert: it's already in the oven), I'll be using the following ingredients:

– Microsoft Teams Webhook (an ordinary one)
– Adaptive Cards Generator (to add a touch of design flair to my code routine)
– Bash (same Bash we all beloved)
– GitLab Includes (to make all things better)


Microsoft Teams Webhook

Let's set up a Teams channel for receiving notifications. To get started:
Click on the three-dot icon to open the settings pop up.
From there, navigate to the Connectors item and search for Incoming Webhook. Create it! It's simple. Eventually you'll get the link like so:

https://mycorp.webhook.office.com/webhookb2/fe6f581b-c5ad-463b-b1f7-9d3897a321ee@5f98fadd-e7df-4de5-af15-f3da802bab2a/IncomingWebhook/1cxf37d5a64f4564ba6a294d161b8139a/8e1cxe4e-14b9-4910-b270-f9c0a2e0c822
Enter fullscreen mode Exit fullscreen mode

Adaptive Cards Generator

We can send our notification as plain text, just like this:

John Doe has just pushed tag 0.4.0 of the React Frontend project, and the CI/CD pipeline has been completed successfully.

It's great if you're a big fan of minimalistic formatting and your coworkers also do appreciate it. However, if this doesn't apply, you should check out Adaptive Cards!

What is that? πŸ€”

Adaptive Cards are easy-to-use UI snippets that can be shared between apps and services. They're written in JSON and automatically adapt to their surroundings when displayed in an app. This makes it simple to create lightweight UI for diverse platforms and frameworks.

Microsoft created Adaptive Cards Designer to provide simple forms/snippets creating tools for their products, such as Teams, WPF, ReactNative and even Skype.

Click the link above and choose Microsoft Teams as the host app and Dark Theme as the color scheme (optional). Then, we can start designing our UI using the interface by directly editing the code. Then, depending on your distinctive requirements, you can achieve a final result comparable to my masterpiece:

Image description

This minimalistic and informative design shows the commit author, tag, and pipeline status.

You might think that you only need to copy the generated code and make a CURL request to Teams' webhook. However, that's not necessarily the case. When I tried it initially, I received a wrong HTTP response. There may be an issue with my Teams settings or policies that I couldn't resolve fully. Fortunately, I came across a case where the author encountered a similar problem and provided a working Adaptive Card template, which you can see below:

{  
   "type":"message",  
   "attachments":[  
      {  
         "contentType":"application/vnd.microsoft.card.adaptive",  
         "contentUrl":null,  
         "content":{  
            "$schema":"http://adaptivecards.io/schemas/adaptive-card.json",  
            "type":"AdaptiveCard",  
            "version":"1.2",  
            "body":[  
                {  
                "type": "TextBlock",  
                "text": "For Samples and Templates, see [https://adaptivecards.io/samples](https://adaptivecards.io/samples)"  
                },  
                {  
                    "type": "Image",  
      "url": "https://adaptivecards.io/content/cats/1.png"      
                }  
            ]  
         }  
      }  
   ]  
}  
Enter fullscreen mode Exit fullscreen mode

To use this template, replace the body: [] part with the code you generated from the adaptivecards.io website. The final result should look something like this:

    {
      "type": "message",
      "attachments": [
        {
          "contentType": "application/vnd.microsoft.card.adaptive",
          "contentUrl": null,
          "content": {
            "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
            "type": "AdaptiveCard",
            "version": "1.2",
            "body": [
              {
                "type": "TextBlock",
                "size": "Medium",
                "weight": "Bolder",
                "text": "My GitLab Project Path β†’ Subpath β†’ Project Name"
              },
              {
                "type": "ColumnSet",
                "columns": [
                  {
                    "type": "Column",
                    "items": [
                      {
                        "type": "Image",
                        "style": "Person",
                        "url": "https://about.gitlab.com/images/press/press-kit-icon.svg",
                        "size": "Medium"
                      }
                    ],
                    "width": "auto"
                  },
                  {
                    "type": "Column",
                    "items": [
                      {
                        "type": "TextBlock",
                        "spacing": "None",
                        "weight": "normal",
                        "text": "**Author**: Maksim Muravev,
                        "isSubtle": false,
                        "wrap": true
                      },
                      {
                        "type": "TextBlock",
                        "spacing": "None",
                        "weight": "normal",
                        "text": "**Tag**: 0.4.0",
                        "isSubtle": false,
                        "wrap": true
                      },
                      {
                        "type": "TextBlock",
                        "spacing": "None",
                        "weight": "normal",
                        "text": "**Status**: SUCCESS βœ…",
                        "isSubtle": false,
                        "wrap": true
                      },
                    ],
                    "width": "stretch"
                  }
                ]
              },
              {
                "type": "TextBlock",
                "text": "Pipeline SUCCESS. See more.",
                "wrap": true
              }
            ]
          }
        }
      ]
    }
Enter fullscreen mode Exit fullscreen mode

As you can see from the example above, you can use markdown inside JSON fields to make the layout look more appealing.

Bash

To send this JSON payload to the webhook's URL and trigger Teams to message you, one can use the following Bash command:

curl -H "Content-Type: application/json" -d '<your JSON payload>' <webhook URL>
Enter fullscreen mode Exit fullscreen mode

That's what we gonna do inside the GitLab CI pipeline.

GitLab Includes

This is the most essential and mind-bending part of the notification process.

We will use Bash command inside a GitLab CI pipeline to send notifications. However, before using it, we must substitute some data in the JSON payload, such as "Tag: 0.4.0", with actual data from the GitLab project.

Thankfully, GitLab provides predefined variables that we can use to access data like the tag, author, and project path via environmental variables inside a pipeline.

Substitution technique will be realized via cat << EOF > concept to make code tidier due to quite massive JSON size.

So let's hypothetically imagine how the job can look alike:

teams-notification:
  image: debian:bullseye
  variables:
    TEAMS_WEBHOOK_URL: "https://mycorp.webhook.office.com/webhookb2/fe6f581b-c5ad-463b-b1f7-9d3897a321ee@5f98fadd-e7df-4de5-af15-f3da802bab2a/IncomingWebhook/1cxf37d5a64f4564ba6a294d161b8139a/8e1cxe4e-14b9-4910-b270-f9c0a2e0c822"
  script: |
    cat << EOF > payload.json
    {
      "type": "message",
      "attachments": [
        {
          "contentType": "application/vnd.microsoft.card.adaptive",
          "contentUrl": null,
          "content": {
            "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
            "type": "AdaptiveCard",
            "version": "1.2",
            "body": [
              {
                "type": "TextBlock",
                "size": "Medium",
                "weight": "Bolder",
                "text": "${CI_PROJECT_PATH//\// β†’ }"
              },
              {
                "type": "ColumnSet",
                "columns": [
                  {
                    "type": "Column",
                    "items": [
                      {
                        "type": "Image",
                        "style": "Person",
                        "url": "https://about.gitlab.com/images/press/press-kit-icon.svg",
                        "size": "Medium"
                      }
                    ],
                    "width": "auto"
                  },
                  {
                    "type": "Column",
                    "items": [
                      {
                        "type": "TextBlock",
                        "spacing": "None",
                        "weight": "normal",
                        "text": "**Author**: ${GITLAB_USER_NAME}",
                        "isSubtle": false,
                        "wrap": true
                      },
                      {
                        "type": "TextBlock",
                        "spacing": "None",
                        "weight": "normal",
                        "text": "**Tag**: : ${CI_COMMIT_REF_NAME}",
                        "isSubtle": false,
                        "wrap": true
                      },
                      {
                        "type": "TextBlock",
                        "spacing": "None",
                        "weight": "normal",
                        "text": "**Status**: ${STATUS^^} ${ICON}",
                        "isSubtle": false,
                        "wrap": true
                      },
                    ],
                    "width": "stretch"
                  }
                ]
              },
              {
                "type": "TextBlock",
                "text": "Pipeline ${STATUS}. [See more](${CI_PIPELINE_URL}).",
                "wrap": true
              }
            ]
          }
        }
      ]
    }
    EOF
    curl -H 'Content-Type: application/json' -d @payload.json $TEAMS_WEBHOOK_URL
Enter fullscreen mode Exit fullscreen mode

Impressive. Let's break this example down into "molecules".

cat << EOF > payload.json

Here we append everything to the payload.json file until EOF happen.

"text": "${CI_PROJECT_PATH//\// β†’ }"

Nicest one. I'm using Bash shell parameter expansion to replace a character in a variable. Initially, GitLab provides $CI_PROJECT_PATH equals to main_project/sub_project/project_name. Because it looks not user-friendly, I decided to replace each slash with a right arrow surrounded by spaces. It's sed-like syntax but inside variable expansion. After replacement it'll look much better: main_project β†’ sub_project β†’ project_name.

"text": "Author: ${GITLAB_USER_NAME}"

Commit's author.

"text": "Tag: : ${CI_COMMIT_REF_NAME}"

Pushed tag by that's commit's author.

"text": "Status: ${STATUS^^} ${ICON}"

Getting the pipeline status can be thorny, especially since no GitLab predefined variable available to indicate the status. Additionally, determining the pipeline status within the pipeline itself becomes complicated πŸ€”. This means the pipeline is still unfinished (because of our job) to decide whether it succeeds or fails πŸ€”πŸ€”πŸ€”.

However, there is a solution 😎. It's based on two statements.

  1. We won't decide whether the pipeline succeeded or failed based on the entire pipeline termination status. We will accept the rule if one job is failed, so the whole pipeline is failed. It satisfies 99% percent of pipelines (by the way, if not, there is a workaround for it).
  2. We will use rules.when GitLab term to decide whether the previous job failed. And if it's failed (or not), the appropriate job will be triggered with arguments needed.

Yes, sounds complicated, but not really. See the example below.

GitLab Teams Notification Stage

Let's create the stage called notify-on-teams. It must be latest in the pipeline to catch potential fails of previous ones.


.notification_template:
  image: debian:bullseye
  variables:
    TEAMS_WEBHOOK_URL: "https://mycorp.webhook.office.com/webhookb2/fe6f581b-c5ad-463b-b1f7-9d3897a321ee@5f98fadd-e7df-4de5-af15-f3da802bab2a/IncomingWebhook/1cxf37d5a64f4564ba6a294d161b8139a/8e1cxe4e-14b9-4910-b270-f9c0a2e0c822"
  script: |
    cat << EOF > payload.json
    {
      "type": "message" ...
      ... # here is the rest of our job as above

teams_notification_when_pipeline_failed:
  stage: notify_on_teams
  extends:
    - .notification_template
  rules:
   - when: on_failure
     if: "$CI_COMMIT_TAG"
     variables:
       STATUS: "failed"
       ICON: "❌"

teams_notification_when_pipeline_succeed:
  stage: notify_on_teams
  extends:
    - .notification_template
  rules:
   - when: on_success
     if: "$CI_COMMIT_TAG"
     variables:
       STATUS: "success"
       ICON: "βœ…"
Enter fullscreen mode Exit fullscreen mode

Yes, I use YAML anchors, but it's not rocket science. Just reusing the code.

As you can see, a final job runs if the previous job succeeds or fails. It runs only when the tag is pushed. Also, it conditionally (whether on_failure or on_success) passes the variable (status as plain text and emoji) to our main template.

And ta da! That's it. Hold on... do you remember word "Includes" in the header? We can make it more elegant using GitLab Includes. Why? To simplify process of adding all that snipets to each GitLab repositories which can be hundreds.

Includes

  • Let's create a separate repository in GitLab called ci-templates.
  • Add files while maintaining a similar structure to this one:
.
└── teams
    β”œβ”€β”€ README.md
    └── notifications.yaml
Enter fullscreen mode Exit fullscreen mode
  • In the notification.yaml file, define our pipeline and include all the code below it. Then commit and push the changes.
.notification_template:
  image: debian:bullseye
  variables:
    TEAMS_WEBHOOK_URL: "https://mycorp.webhook.office.com/webhookb2/fe6f581b-c5ad-463b-b1f7-9d3897a321ee@5f98fadd-e7df-4de5-af15-f3da802bab2a/IncomingWebhook/1cxf37d5a64f4564ba6a294d161b8139a/8e1cxe4e-14b9-4910-b270-f9c0a2e0c822"
  script: |
    cat << EOF > payload.json
    {
      "type": "message"
      ... # here is the rest of our job as above


.pipeline_failed:
  extends:
    - .notification_template
  rules:
   - when: on_failure
     if: "$CI_COMMIT_TAG"
     variables:
       STATUS: "failed"
       ICON: "❌"

.pipeline_succeed:
  extends:
    - .notification_template
  rules:
   - when: on_success
     if: "$CI_COMMIT_TAG"
     variables:
       STATUS: "success"
       ICON: "βœ…"
Enter fullscreen mode Exit fullscreen mode
  • Let's finally use the Include in child repositories:
include:
  - project: 'ci-templates'
    ref: master
    file:
      - '/teams/notifications.yaml'

# Don't forget about stages:
stages:
  # - lint_python
  # - test_python
  # - build_docker
  - teams_notifications  # last stage

# Finally, create two jobs and extend the templates:
teams_notifications_failed:
  stage: teams_notifications
  extends:
    - .pipeline_failed

teams_notifications_success:
  stage: teams_notifications
  extends:
    - .pipeline_succeed
Enter fullscreen mode Exit fullscreen mode

That's it.

Top comments (1)

Collapse
 
ala1977 profile image
ala1977

good