writing a Github App in Node.js

The developer is a creative person. He has no time for routine tasks that a machine can take care of. Therefore, everything that can be automated must be automated.

Hey! My name is Nikita. I am a developer Taiga UI, a library of Angular components, which is actively used in our Tinkoff company. I will tell you about solving one of such routine tasks on our project by writing your own Github App on Node.js.

Formulation of the problem

On the project, we are actively writing screenshot tests using the framework Cypress.

After making edits to the code and opening the Pull Request in CI, the Github Workflow begins to run all the tests, which save our future release from introducing bugs into the UI Kit components. As soon as any test fails, all screenshots are attached by the archive as artifacts to this workflow, which the developer can download and study. Unfortunately, we do not live in a perfect world where we do not make mistakes and tests periodically fail. When there are too many tests, such a seemingly simple action as downloading an archive and searching for screenshots with differences in “before” / “after” states becomes a grueling task. And how cool it would be if there was an opportunity to simplify this process!

Cypress has an official paid tool for this – Dashboard… But the rates that are offered there seem to be prohibitively expensive for our needs.

There is an alternative unofficial solution that has gained popularity – Sorry cypress… Its authors offer their own version of the Dashboard, but with lower prices or with the ability to host the entire infrastructure on their servers. This unofficial version already seems more acceptable. But we decided to write a simple Github bot.

How Github Apps work

To simplify a lot, the Github App is a set of callback functions that are called when the desired event (webhook-event) is triggered in the repository. A list of all available events is presented on the Github Docs page… The callback functions themselves usually pull the Github API internally, which leads to the creation of new comments, branches, files, etc. in the repository.

All work with listening to the necessary events and sending the necessary API requests can be done in native js. But it is much easier to do this with the help of ready-made popular solutions that provide some abstraction from all this. We will use the framework Probotcreated for writing Github applications.

The process of initializing a new application through cli commands and the steps for launching it are well described. on the official page of the framework, we will not disassemble them. When creating an application, we recommend choosing a template written in Typescript: strong typing will allow you to avoid some mistakes (for how you can get the most out of the capabilities of this language, read in this article). We will also use a Typescript template when creating the current application.

Listening for repository events

Our bot only needs to listen to three types of events: when the workflow starts and ends, and when the PR is closed. Open in the generated application index.ts file and add the following code:

import {Probot} from 'probot';

export = (app: Probot) => {
    app.on('workflow_run.requested', async context => {
       // ...
    });

    app.on('workflow_run.completed', async context => {
       // ...
    });

    app.on('pull_request.closed', async context => {
       // ...
    });
};

Note: do not forget in Github on the application settings page to give the bot rights to listen for events workflow_run and pull_request

You can see in the code that each function that is passed as a callback to repository events takes an argument context

This context contains a lot of useful information about the “listening” event. For example, this is what the selector utility will look like to get the name of the workflow that called the given webhook-event:

import {Context} from 'probot/lib/context';
import {
  EventPayloads
} from '@octokit/webhooks/dist-types/generated/event-payloads';

type WorkflowRunContext = Context<EventPayloads.WebhookPayloadWorkflowRun>;

export const getWorkflowName = (context: WorkflowRunContext): string =>
	context.payload.workflow?.name || '';

Also inside context.payload contains the information we need: id of the workflow, the name of the branch on which this workflow was triggered, the number of the open pull request, and a lot of other information.

Using the Github API

The Probot framework internally uses Node.js module ‘@ octokit / rest’… To access the REST API methods on Github, just refer to context.octokit…… The entire list of available actions look here

Our bot needs the following methods to create comments on PR:

  1. context.octokit.issues.createComment (create a new comment to PR).

  2. context.octokit.rest.issues.updateComment (edit the content of an already existing comment to the PR).

Don’t be confused by the fact that we are using methods from an object. issue… Pull request is an issue containing code. Therefore, all methods applicable to the issue apply to pull requests as well.

To load artifacts with screenshots of failed tests, we use the following methods:

  1. context.octokit.actions.listWorkflowRunArtifacts (a list of meta information about the available artifacts of a given workflow).

  2. context.octokit.actions.downloadArtifact (loading artifact archives by their id).

So, we have screenshot files, and we know how to create comments. Comments understand Markdown syntax, and images can be inserted into this format as base64 strings. It seems that another half step – and everything will be ready … but no. Markdown, which Github uses, does not support the ability to insert images this way: only by link from an external source.

But this problem can be solved: you can upload the required file (which we plan to attach to the report on the failed tests) to a separate branch of the repository and access this image through https://raw.githubusercontent.com/...… The code to solve this problem will be as follows:

const GITHUB_CDN_DOMAIN = 'https://raw.githubusercontent.com';

const getFile = async (path: string, branch?: string) => {
   return context.octokit.repos.getContent({
       ...context.repo(),
       path,
       ref: branch
   }).catch(() => null);
}

// returns url to uploaded file
const uploadFile = async ({file, path, branch, commitMessage}: {
    file: Buffer,
    path: string,
    commitMessage: string,
    branch: string
}): Promise<string> => {
    const {repo, owner} = context.repo();
    const content = file.toString('base64');
    const oldFileVersion = await getFile(path, branch);
    const sha = oldFileVersion && 'sha' in oldFileVersion.data
        ? oldFileVersion.data.sha
        : undefined
    const fileUrl = `${GITHUB_CDN_DOMAIN}/${owner}/${repo}/${branch}/${path}`;

    return context.octokit.repos
        .createOrUpdateFileContents({
            owner,
            repo,
            content,
            path,
            branch,
            sha,
            message: commitMessage,
        })
        .then(() => fileUrl);
}

// returns urls to uploaded images
const uploadImages = async (
  images: Buffer[],
	pr: number,
	workflowId: number,
	i: number
): Promise<string[]> => {
   const {repo, owner} = context.repo();
   const path = `__bot-screens/${owner}-${repo}-${pr}/${workflowId}-${i}.png`;

   return Promise.all(images.map(
       (file, index) => uploadFile({
           file,
           path,
           commitMessage: 'chore: upload images of failed screenshot tests',
           branch: 'screenshot-bot-storage',
       })
   ));
}

Once the PR is closed, uploaded images can always be deleted. And for all these actions there are also methods in the library @ octokit / rest

Deploy the finished code

Deploy is an inevitable stage in the life of every application. The official documentation of the Probot framework suggests detailed instructionshow to deploy your finished application to various popular services. We deployed our Node.js application on Glitch. This service provides free hosting, and the limitations of a free account are irrelevant for a simple application like a Github bot.

The source code of the resulting bot, which we actively use in our project, can be examined on the Github repository. The development was named Argus (a multi-eyed giant from ancient Greek mythology). It has been worked out much deeper than can be described in this article, but the main core of the resulting application was described above.

Instead of a conclusion

Writing a Github bot is very easy. It requires almost no deep knowledge of the language or framework. The whole process of creation mainly comes down to studying the documentation. with a list of webhook events repository as well as documentation Github REST API Methodsto find and apply them for your task.

In this article, we have built a Github application that follows a workflow containing screenshot tests. If the tests fail, the bot downloads artifacts, finds screenshots with the difference between the “before” / “after” states, and then attaches them as a comment to the PR.

We deployed the resulting code as a bot called Lumberjack (lumberjack). He is already actively following our project. Taiga UI. But the bot is written in such a way that you can easily customize it for your project – just invite it to your repository and indicate which workflow it should follow.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *