Wasm Builders 🧱

Cover image for Create a GitLab bot with a Capsule Wasm function in less than 100 lines of code
Philippe Charrière
Philippe Charrière

Posted on

Create a GitLab bot with a Capsule Wasm function in less than 100 lines of code

What do I want to achieve?

In a GitLab project, when I create an issue or add a note to an issue, I would like that a bot automatically adds a note with a message for me.

What is capsule: Capsule is a WebAssembly function runner: https://github.com/bots-garden/capsule#what-is-capsule

What do I need?

  • A GitLab Webhook: every time I invoke the bot by its name in the body of the issue or in a note (a note is a comment), it will trigger a GitLab Webhook.
  • A Capsule HTTP service (the bot): the service will be invoked by the webhook.
  • The bot will be able to use the GitLab API to add notes to the issue.

First, let's create the webhook

In a GitLab project, go to the Settings>Webhooks menu (left side menu) and set the values of the form. You need:

  • The url of your bot (the HTTP service). (*)
  • Select the trigger events: Comments and Issue events

webhook creation

(*): I use I use Gitpod to develop. The advantage is that I can launch my service from Gitpod and obtain a public URL to make it accessible with the 'toto' command. Otherwise, to test your bot locally, you can use a tunnelling service like Ngrok.

Then click on the Add webhook button.

We need a bot user

You need to create a GitLab user (in addition to your own user) (*). Then generate a Personal Access Tokens for this user, and give the API rights to the token:

Personal Access Tokens

Now, the bot user is able to use the GitLab API, and then is able to add notes to an issue.

  • 🖐 keep the token somewhere
  • 🤖 add the user to the members list of your project

No we can create the code of the bot service

Pre-requisites

Generate the skeleton code of the bot:

When the webhook is triggered, it does a POST HTTP request, then, we will create a function callable with a POST HTTP request (service-post):

cabu generate service-post bob
cd bob
go mod tidy
Enter fullscreen mode Exit fullscreen mode

when cabu is run for the first time, it will pull the Capsule Builder Docker image

Change the code of the function

The Capsule Runner brings some useful host functions to the wasm module, and especially:

  • hf.Http to make HTTP requests
  • hf.GetEnv to read the value of the environment variables.

hf.GetEnv is extremely important, because it will help to make your bot service "cloud compliant": have a look at: The twelve-factor app stores config in environment variables

This is the final source code:

package main

import (
  "strconv"
  "strings"

  hf "github.com/bots-garden/capsule/capsulemodule/hostfunctions"
  "github.com/tidwall/gjson"
)

const (
  Open   string = "open"   // new issue
  Close         = "close"  // issue or comment
  Reopen        = "reopen" // issue
  Update        = "update" // issue or comment
)

const (
  Issue = "issue"
  Note  = "note"
)

func main() {
  hf.SetHandleHttp(Handle)
}

func addNoteToTheIssue(issueIid float64, projectId float64, message string) (string, error) {

  botToken, _ := hf.GetEnv("BOT_TOKEN")
  apiUrl, _ := hf.GetEnv("API_URL")

  issueNumber := strconv.FormatInt(int64(issueIid), 10)
  projectNumber := strconv.FormatInt(int64(projectId), 10)

  hf.Log("🖐 issueNumber: " + issueNumber)
  hf.Log("🖐 projectNumber: " + projectNumber)

  noteApiUrl := apiUrl + "/projects/" + projectNumber + "/issues/" + issueNumber + "/notes"

  headers := map[string]string{
    "Content-Type":  "application/json; charset=utf-8",
    "PRIVATE-TOKEN": botToken,
  }

  jsondoc := `{"body": "` + message + `"}`

  return hf.Http(noteApiUrl, "POST", headers, jsondoc)
}

func Handle(request hf.Request) (response hf.Response, errResp error) {
  var resp string
  var err error

  object := gjson.Get(request.Body, "object_kind") // it should be issue or note

  objectAttributes := gjson.Get(request.Body, "object_attributes") // issue or comment

  projectId := gjson.Get(request.Body, "project.id").Num

  userName := gjson.Get(request.Body, "user.username").Str 

  botName, _ := hf.GetEnv("BOT_NAME")

  if object.Str == Issue {

    action := objectAttributes.Get("action") // open close reopen update (only for issue)

    if action.Str == Open || action.Str == Update {
      issueDescription := objectAttributes.Get("description").Str
      issueIid := objectAttributes.Get("iid").Num

      if strings.Contains(issueDescription, botName) {
        resp, err = addNoteToTheIssue(issueIid, projectId, "👋 @"+userName+" what's up? 😄")
      }
    }
  }

  // a comment is added to the issue or an existing comment is updated
  if object.Str == Note {

    note := objectAttributes.Get("note").Str
    issueIid := gjson.Get(request.Body, "issue.iid").Num

    if strings.Contains(note, botName) {
      resp, err = addNoteToTheIssue(issueIid, projectId, "🤔 @"+userName+" are you talking to me?")
    }
  }

  headersResp := map[string]string{
    "Content-Type": "application/json; charset=utf-8",
  }

  if err != nil {
    hf.Log("😡 error:" + err.Error())
  }

  return hf.Response{Body: resp, Headers: headersResp}, err
}
Enter fullscreen mode Exit fullscreen mode

Build the Wasm Bot module

cd bob
cabu build . bob.go bob.wasm
Enter fullscreen mode Exit fullscreen mode

Start the Wasm Bot module

BOT_NAME="@swannou" \
BOT_TOKEN="${SWANNOU_TOKEN}" \
API_URL="https://gitlab.com/api/v4" \
capsule -wasm=./bob.wasm -mode=http -httpPort=8080
Enter fullscreen mode Exit fullscreen mode

Let's return to your GitLab project, and play!

Create an issue and notify the bot:
create an issue

The first answer of the bot:
the first answer

Add a note and notify the bot:
add notes

Of course, you can create more intelligent bots to analyze text, detect spelling mistakes, and provide help, ...

You can find the source code of the GitLab bot here: https://github.com/bots-garden/bob-the-bot/tree/main/bob-gitlab (and a GitHub bot version there: https://github.com/bots-garden/bob-the-bot/tree/main/bob-github)

That's it! And Have fun 🙂

If you want to read more about:

Photo by Kenny Eliason on Unsplash

Top comments (0)