Engineering

Building a GitHub Issue Recommendation Bot with Algolia
facebooklinkedintwittermail

GitHub Issues are static content. What if they didn’t have to be?

When we (DevRels Chuck Meyer and Bryan Robinson) discovered that Dev.to was hosting a GitHub Actions hackathon, we knew we needed to give it a try.

We knew we wanted to figure out a useful tool to integrate Algolia into an Action. There were obvious thoughts on what sort of project to undertake. We thought through the usual takes on indexing content, products, or markdown. They all would have been helpful for web creators. Would they have been helpful to open source maintainers, though? Probably?

How could we make their overall workflow better?

Then it struck us: What if we could give recommended Issues for frequently asked questions? Could we lessen the burden of maintainers answering similar questions? How many Issues get closed as “duplicate” in large repositories? Could Algolia provide Issue creators a list of related, helpful Issues?

Spoiler alert: Yeah, totally!

The Workflow structure

When a developer adds an Issue to a repository, we need to execute three steps.

First, we need to search an Algolia Index for related Issues. Then, we bundle those results into Markdown and pass it to an Action to create a comment on the initial Issue. Finally, we need to put the Issue into our Index for future searches.

Each of these steps requires an Action. The Algolia-specific Actions, we needed to create from scratch. The comment-writing Action, we decided to use the amazing Peter Evan’s create-or-update-comment Action – which, as it turns out, GitHub uses in many of their docs about Actions.

Let’s dive into the new Actions.

Performing a search query

The first step of our Workflow is a search query sent to Algolia. We created a custom Action for this Get Algolia Issue Records.

To use the Action, we need to send it four required inputs (and an optional fifth).

  • app_id: the ID of the application in your Algolia account. This is best stored as a Secret in your repository
  • api_key: An API key with search permissions to the Index in your Algolia App. This is best stored in a Secret in your repository.
  • index_name: The name of the Algolia Index to search. For consistency, we recommend the github.event.repository.name variable.
  • issue_title: The title of the inciting Issue found with github.event.issue.title.
  • max_results: (OPTIONAL) A number of results to return to the comment (defaults to 3)

We take these variables and perform a similarQuery search based on the inciting Issue’s title. We then create a comment body and a list of items in Markdown (the format needed for GitHub comments). This outputs are passed to Peter Evans’ create-or-update-comment Action.

const { inspect } = require('util');
const core = require('@actions/core');
const algoliasearch = require('algoliasearch');

async function run() {
  try {
    const inputs = {
      appId: core.getInput('app_id'),
      apiKey: core.getInput('api_key'),
      indexName: core.getInput('index_name'),
      issueTitle: core.getInput('issue_title'),
      maxResults: core.getInput('max_results'),
    };
    core.info(`Inputs: ${inspect(inputs)}`);

    if (!inputs.appId && !inputs.apiKey && !inputs.indexName) {
      core.setFailed('Missing one or more of Algolia app id, API key, or index name.');
      return;
    }

    inputs.maxResults = inputs.maxResults || 3;

    const client = algoliasearch(inputs.appId, inputs.apiKey);
    const index = client.initIndex(inputs.indexName);

    index.search('', { 
        similarQuery: inputs.issueTitle,
        hitsPerPage: inputs.maxResults
      }).then(({hits}) => {
      core.info(`Searching for record`);
      core.info(`Hits: ${inspect(hits)}`);
      const message = `## Other issues similar to this one:\n${hits.map(hit => `* [${hit.title}](${hit.url})`).join('\n')}`
      const listItems = `${hits.map(hit => `* [${hit.title}](${hit.url})`).join('\n')}\n`
      core.info(message)
      core.info(listItems)
      core.setOutput('comment_body', message);
      core.setOutput('issues_list', listItems);
    })
      .catch(err => {
        core.setFailed(err.message);
      }
    );
  } catch (error) {
    core.debug(inspect(error));
    core.setFailed(error.message);
    if (error.message == 'Resource not accessible by integration') {
      core.error(`See this action's readme for details about this error`);
    }
  }
}

run();

Adding the issue to your Algolia index

For the final step of our workflow, we add this new issue to the Algolia index for future searches. We created another GitHub Action for this purpose: Create or Update Algolia Index Record. This action atomically adds/updates a record directly to an index rather than writing/reading from a JSON file. This makes sense in situations where we are acting on metadata about the repo (issues, pull requests, comments) as opposed to building an index for the application itself.

To use this action, we’ll need to create an Algolia API key with permissions to add/update records in our index. Additionally, we will need permission to create a new index for the repo. Otherwise, we must create it ahead of time and hard code the index name in our configuration.

Along with the new API key we’ll need a few other inputs to use the action:

  • app_id: You should already have this as a Secret in your repository from the action above
  • api_key: This is the new key with permission to save records to your index. This is best stored in a Secret in your repository.
  • index_name: The name of the Algolia index to add/update this record. For consistency, we recommend the github.event.repository.name variable.
  • record: A string represneting the JSON record to add to the index.

If the API key has permission, the action creates an index for the repository. We’ll add the issue title and URL (to link back) as the record. It’s a multi-line string in our workflow, but must_ be valid JSON for the action to work (see Preparing your data for details).

We take all of these inputs and execute a saveObject call via the Algolia API. We use the issue ID as the objectID in the index. This makes it easy to tie the record back to this issue if we add workflows for update or delete events later.

const { inspect } = require('util');
const core = require('@actions/core');
const algoliasearch = require('algoliasearch');

async function run() {
  try {
    const inputs = {
      appId: core.getInput('app_id'),
      apiKey: core.getInput('api_key'),
      indexName: core.getInput('index_name'),
      record: core.getInput('record'),
    };
    core.debug(`Inputs: ${inspect(inputs)}`);

    if (!inputs.appId && !inputs.apiKey && !inputs.indexName) {
      core.setFailed('Missing one or more of Algolia app id, API key, or index name.');
      return;
    }

    core.info(`Writing record to index ${inputs.indexName}`)
    const client = algoliasearch(inputs.appId, inputs.apiKey);
    const index = client.initIndex(inputs.indexName);

    index.saveObject(JSON.parse(inputs.record), {'autoGenerateObjectIDIfNotExist': true})
      .then(({ objectID }) => {
        core.setOutput('object_id', objectID);
        core.info(
          `Created record in index ${inputs.indexName} with objectID ${objectID}.`
        );
      })
      .catch((err) => {
        core.setFailed(`Failed to save object: ${err}`);
      });

  } catch (error) {
    core.debug(inspect(error));
    core.setFailed(error.message);
    if (error.message == 'Resource not accessible by integration') {
      core.error(`See this action's readme for details about this error`);
    }
  }
}

run();

Next, we piece the two new Actions together with the existing comment creation action to build our workflow.

The full workflow file

To make this work, we need one job with three steps. Each step will use one of these Actions.

name: related-issues
on:
  # Triggers the workflow on push or pull request events but only for the main branch
  issues:
    types: 
      - opened

jobs:
  get-related-issues:
    permissions: 
      # Gives the workflow write permissions only in issues
      issues: write
    runs-on: ubuntu-latest
    steps:
      # Performs a search in an Algolia Index based on Issue Title
      # The Index should have historical Issues
      # Returns two outputs:
      # issues_list: a markdown list of issues
      # comment_body: a generic comment body with the list of issues
      - id: search
        name: Search based on issue title
        uses: brob/algolia-issue-search@v1.0
        with: 
          # Requires an Algolia account with an App ID and write key
          app_id: ${{ secrets.ALGOLIA_APP_ID }}
          api_key: ${{ secrets.ALGOLIA_API_KEY }}
          index_name: ${{ github.event.repository.name }}
          issue_title: ${{ github.event.issue.title }}
      - name: Create or Update Comment
        uses: peter-evans/create-or-update-comment@v1.4.5
        with:
          # GITHUB_TOKEN or a repo scoped PAT.
          token: ${{ github.token }}
          # The number of the issue or pull request in which to create a comment.
          issue-number: ${{ github.event.issue.number }}
          # The comment body. Can use either issues_list or comment_body
          body: |
            # While you wait, here are related issues:
            ${{ steps.search.outputs.issues_list }}
            Thank you so much! We'll be with you shortly!
      # An Action to create a record in an Algolia Index
      # This is a generic Action and can be used outside of this workflow
      - name: Add Algolia Record
        id: ingest
        uses: chuckmeyer/add-algolia-record@v1
        with:
          app_id: ${{ secrets.ALGOLIA_APP_ID }}
          api_key: ${{ secrets.ALGOLIA_API_KEY }}
          index_name: ${{ github.event.repository.name }}
          # Record needs to be a string of JSON
          record: |
            {
              "title": "${{ github.event.issue.title }}", 
              "url": "${{ github.event.issue.html_url }}", 
              "labels": "${{ github.event.issue.labels }}",
              "objectID": "${{ github.event.issue.number }}"
            }

Next steps

We hope that this is helpful to maintainers, but we also hope it inspires others to find better and better ways to suggest content in static areas like GitHub Issues.

If you want to play around with the full workflow, you can check it out in this repository. Both the search and ingest Actions are available in the GitHub marketplace.

Search and discovery can become an interesting part of your automated workflow in GitHub and beyond.Check out related solutions on our open source code exchange platform.


About the authorsBryan Robinson

Bryan Robinson

Senior Developer Relations Specialist
Chuck Meyer

Chuck Meyer

Sr. Developer Advocate

Recommended Articles

Powered by Algolia AI Recommendations

How to responsibly give a chatbot access to a database
AI

How to responsibly give a chatbot access to a database

Jaden Baptista

Jaden Baptista

Freelance Writer at Authors Collective
Indexing Markdown content with Algolia
Engineering

Indexing Markdown content with Algolia

Michael King

Michael King

Developer Advocate
Soma Osvay

Soma Osvay

Full Stack Engineer, Starschema
How to use Algolia as a game engine debugging tool in Rust
Engineering

How to use Algolia as a game engine debugging tool in Rust

Gyula László

Gyula László

Senior Developer @ Starschema