How to configure GitLab Runner to send DingTalk robot webhook notifications after each run

Background

We want to notify a DingTalk robot via webhook after each GitLab Runner execution, informing users about what the runner did and which commit version was used. The desired effect is shown in the image below:

Write the Code to Call the DingTalk Webhook

Refer to DingTalk’s official Python code:

#!/usr/bin/env python  

import argparse  
import logging  
import time  
import hmac  
import hashlib  
import base64  
import urllib.parse  
import requests  

def setup_logger():  
    logger = logging.getLogger()  
    handler = logging.StreamHandler()  
    handler.setFormatter(  
        logging.Formatter('%(asctime)s %(name)-8s %(levelname)-8s %(message)s [%(filename)s:%(lineno)d]'))  
    logger.addHandler(handler)  
    logger.setLevel(logging.INFO)  
    return logger  

def define_options():  
    parser = argparse.ArgumentParser()  
    parser.add_argument(  
        '--access_token', dest='access_token', required=True,  
        help='The robot webhook access_token from https://open.dingtalk.com/document/orgapp/obtain-the-webhook-address-of-a-custom-robot '  
    )  
    parser.add_argument(  
        '--secret', dest='secret', required=True,  
        help='The secret from https://open.dingtalk.com/document/orgapp/customize-robot-security-settings#title-7fs-kgs-36x'  
    )  
    parser.add_argument(  
        '--userid', dest='userid',  
        help='DingTalk user ID(s) to @ (comma-separated), from https://open.dingtalk.com/document/orgapp/basic-concepts-beta#title-o8w-yj2-t8x '  
    )  
    parser.add_argument(  
        '--at_mobiles', dest='at_mobiles',  
        help='Mobile number(s) to @ (comma-separated)'  
    )  
    parser.add_argument(  
        '--is_at_all', dest='is_at_all', action='store_true',  
        help='Whether to @ everyone; if specified, it is True; otherwise, False'  
    )  
    parser.add_argument(  
        '--msg', dest='msg', default='DingTalk: Empowering Progress',  
        help='Message content to send'  
    )  
    return parser.parse_args()  

def send_custom_robot_group_message(access_token, secret, msg, at_user_ids=None, at_mobiles=None, is_at_all=False):  
    """  
    Send a custom DingTalk robot group message.  
    :param access_token: Robot webhook access_token  
    :param secret: Signing secret from robot security settings  
    :param msg: Message content  
    :param at_user_ids: List of user IDs to @  
    :param at_mobiles: List of mobile numbers to @  
    :param is_at_all: Whether to @ everyone  
    :return: DingTalk API response  
    """  
    timestamp = str(round(time.time() * 1000))  
    string_to_sign = f'{timestamp}\\n{secret}'  
    hmac_code = hmac.new(secret.encode('utf-8'), string_to_sign.encode('utf-8'), digestmod=hashlib.sha256).digest()  
    sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))  

    url = f'https://oapi.dingtalk.com/robot/send?access_token={access_token}&timestamp={timestamp}&sign={sign}'  

    body = {  
        "at": {  
            "isAtAll": str(is_at_all).lower(),  
            "atUserIds": at_user_ids or [],  
            "atMobiles": at_mobiles or []  
        },  
        "text": {  
            "content": msg  
        },  
        "msgtype": "text"  
    }  
    headers = {'Content-Type': 'application/json'}  
    resp = requests.post(url, json=body, headers=headers)  
    logging.info("DingTalk custom robot group message response: %s", resp.text)  
    return resp.json()  

def main():  
    options = define_options()  
    # Process @ user IDs  
    at_user_ids = []  
    if options.userid:  
        at_user_ids = [u.strip() for u in options.userid.split(',') if u.strip()]  
    # Process @ mobile numbers  
    at_mobiles = []  
    if options.at_mobiles:  
        at_mobiles = [m.strip() for m in options.at_mobiles.split(',') if m.strip()]  
    send_custom_robot_group_message(  
        options.access_token,  
        options.secret,  
        options.msg,  
        at_user_ids=at_user_ids,  
        at_mobiles=at_mobiles,  
        is_at_all=options.is_at_all  
    )  

if __name__ == '__main__':  
    main()  

Write a Python Script to Fulfill Our Requirements

import os  
import sys  
import json  
import time  
import hmac  
import hashlib  
import base64  
import urllib.parse  
import urllib.request  

def main():  
    webhook = os.environ.get("DINGTALK_WEBHOOK", "")  
    if not webhook:  
        print("DINGTALK_WEBHOOK not set, skipping notification.")  
        return 0  

    secret = os.environ.get("DINGTALK_SECRET", "")  
    branch = os.environ.get("CI_COMMIT_REF_NAME", "")  
    commit_sha = os.environ.get("CI_COMMIT_SHORT_SHA", "")  
    commit_msg = os.environ.get("CI_COMMIT_MESSAGE", "").strip()  

    project_url = 'Your GitLab URL'  
    commit_full_sha = os.environ.get("CI_COMMIT_SHA", "")  
    commit_link = f"{project_url}/-/commit/{commit_full_sha}"  
    content = f"Deployment completed successfully.\nBranch used: {branch}\nCommit SHA: {commit_sha}\nCommit message: {commit_msg}\nCommit link: {commit_link}"  

    payload = {"msgtype": "text", "text": {"content": content}}  
    data = json.dumps(payload).encode("utf-8")  

    url = webhook  
    if secret:  
        ts = str(int(time.time() * 1000))  
        string_to_sign = f"{ts}\\n{secret}"  
        h = hmac.new(secret.encode("utf-8"), string_to_sign.encode("utf-8"), digestmod=hashlib.sha256).digest()  
        sign = urllib.parse.quote_plus(base64.b64encode(h).decode("utf-8"))  
        url = f"{webhook}&timestamp={ts}&sign={sign}"  

    try:  
        req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})  
        with urllib.request.urlopen(req, timeout=10) as f:  
            body = f.read().decode("utf-8")  
            print("DingTalk notify success:", body)  
            return 0  
    except Exception as e:  
        print("DingTalk notify failed:", e, file=sys.stderr)  
        return 2  

if __name__ == "__main__":  
    sys.exit(main())  

Invoke This Python Script in GitLab Runner

stages:  
  - notify  

notify-success:  
  stage: notify  
  image: python:3.11-alpine  
  only:  
    - dev  
  script:  
    - |  
      # Send deployment success notification via DingTalk custom robot (using script in the project)  
      python3 scripts/ci_dingtalk_notify.py  

Configure Variables in GitLab CI/CD

Conclusion

Run!