Custom GitLab Pipeline Notifications To Microsoft Teams
Tags: devops, gitlab, tutorial, ci
Published: 2023-04-26 13:18:44

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ve4nhm4bkwk73fb48oqm.png)

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
```

## 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](https://adaptivecards.io/)!

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](https://adaptivecards.io/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8hjmyts5ohp5v9oh7bm6.png)

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:

```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",  
                "text": "For Samples and Templates, see [https://adaptivecards.io/samples](https://adaptivecards.io/samples)"  
                },  
                {  
                    "type": "Image",  
      "url": "https://adaptivecards.io/content/cats/1.png"      
                }  
            ]  
         }  
      }  
   ]  
}  
```

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:

```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": "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
              }
            ]
          }
        }
      ]
    }
```

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:

```bash
curl -H "Content-Type: application/json" -d '<your JSON payload>' <webhook URL>
```

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](https://docs.gitlab.com/ee/ci/variables/predefined_variables.html) 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](https://stackoverflow.com/questions/2500436/how-does-cat-eof-work-in-bash) to make code tidier due to quite massive JSON size.

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

```yaml
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
```

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](https://stackoverflow.com/questions/13210880/replace-one-substring-for-another-string-in-shell-script) 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.

```yaml

.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: "✅"
```

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](https://docs.gitlab.com/ee/ci/yaml/includes.html). 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:
```bash
.
└── teams
    ├── README.md
    └── notifications.yaml
```
- In the `notification.yaml` file, define our pipeline and include all the code below it. Then commit and push the changes.
```yaml
.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: "✅"
```
- Let's finally use the Include in child repositories:
```yaml
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
```

That's it.