FEBRUARY 25 2025
Building a GitHub issue summarizer & knowledge base with Modus + GitHub Actions
Learn how to automatically generate articles for your knowledge base in this walkthrough guide

This guide will walk you through building a GitHub Issue Summarizer using Modus and GitHub Actions. The project automatically generates a knowledge base (KB) article whenever a GitHub issue is closed and posts it as a GitHub Discussion. The app retrieves issue details, reactions, and comments via the GitHub API, then leverages Modus to call an LLM, which generates a structured KB article summarizing the solution. This helps keep important discussions organized, searchable, and easily accessible.
Resources
- GitHub repo: https://github.com/hypermodeinc/modus-recipes/tree/main/modus-gh-issue-summarizer
- Modus docs:https://docs.hypermode.com/modus/overview
- Hypermode docs: https://docs.hypermode.com/introduction
Prerequisites
Before diving in, ensure you have the following:
- Basic knowledge of Go programming
- Modus installed (we'll guide you through this)
- A GitHub account
What Is Modus?
Modus is an open source, serverless framework that simplifies integrating AI models and APIs into applications. It allows developers to focus on building intelligent features without the overhead of complex infrastructure.
Here's why Modus stands out:
- Seamless AI Integration: Modus makes it simple to integrate AI models like LLMs, treating them as first-class components to reduce complexity in development.
- Database Integration: Modus offers robust support for querying and mutating data from databases like Dgraph, enabling the combination of structured data with AI capabilities.
- Advanced Search: Modus provides semantic and similarity-based search capabilities, leveraging vector embeddings to enhance natural language search and recommendations.
- Optimized Performance: Modus is designed for real-time responses, making it ideal for applications that need speed and reliability.
Setting Up Your Modus Project
-
Install the Modus CLI
npm install -g @hypermode/modus-cli
-
Initialize Your Modus App
modus new
This command prompts you to choose between Go and AssemblyScript as the language for your app. In this example, we use Go. Modus then creates a new directory with the necessary files and folders for your app. You will also be asked if you would like to initialize a Git repository.
-
Build & Run Your App
modus dev
This command builds and runs your app locally in development mode and provides you with a URL to access your app's generated API.
-
Access Your Local Endpoint
Once your app is running, you can access the API explorer for your API at the URL located in your terminal, by default at
http://localhost:8686/explorer
This interface allows you to interact with your app's API and test your functions.
Configuring modus.json
The modus.json
file defines how our Modus project is structured, including the LLM model for generating knowledge base articles and the GitHub API connection for fetching issue details and posting discussions.
We configure an LLM model named "generate-article"
, which uses Meta Llama 3.2 via Hugging Face to process issue data and generate structured summaries. Additionally, we set up a GitHub HTTP connection to interact with the API for retrieving issue details, reactions, and comments.
Example modus.json
Configuration:
{
"$schema": "https://schema.hypermode.com/modus.json",
"endpoints": {
"default": {
"type": "graphql",
"path": "/graphql",
"auth": "bearer-token"
}
},
"models": {
"generate-article": {
"sourceModel": "meta-llama/Meta-Llama-3.2-3B-Instruct",
"provider": "hugging-face",
"connection": "hypermode"
}
},
"connections": {
"github": {
"type": "http",
"baseUrl": "https://api.github.com/"
}
}
}
With this configuration, our project is ready to interact with both Modus for AI-powered KB article generation and GitHub for issue data retrieval and discussion creation.
Writing the Core Functions
Now that our project is set up, let's dive into the core functions that power our GitHub Issue Summarizer.
These functions are written in the main.go
file, which serves as the entry point for our Modus application. These functions handle fetching issue data, generating a KB article, and posting it as a discussion on GitHub.
Imports & Type Definitions
At the top of the file, we import the necessary packages:
encoding/json
- For parsing GitHub API responsesfmt
- For formatting output and constructing stringsstrings
- For manipulating textgithub.com/hypermodeinc/modus/sdk/go/pkg/http
- For making HTTP requestsgithub.com/hypermodeinc/modus/sdk/go/pkg/models
andgithub.com/hypermodeinc/modus/sdk/go/pkg/models/openai
- For working with the LLM to generate the KB article
We also define structs to represent GitHub API responses, including:
GitHubUser
- Represents the issue author and comment contributorsGitHubLabel
- Stores issue labelsGitHubReactions
- Tracks reactions (👍 ❤️ 🚀, etc.)GitHubIssue
- Represents an issue with details like title, body, state, and reactionsGitHubComment
- Stores user comments on an issue
These types allow us to structure the API responses in a way that makes them easier to work with.
The IssueClosedHandler
Function
The IssueClosedHandler
function is the entry point of our program and the only exported function.
In Modus, functions are exported when they are capitalized, making them available as GraphQL endpoints. The IssueClosedHandler
function runs when a GitHub issue is closed, triggering the workflow to generate a KB article and post it as a discussion.
Function Parameters:
IssueClosedHandler
accepts the following parameters, which are passed in from the GitHub Action (covered later in this post):
repo
(string) - The full repository name (owner/repo
)issueNumber
(int) - The number of the issuetoken
(string) - A GitHub API token used for authentication when creating discussions
func IssueClosedHandler(repo string, issueNumber int, token string) {
issue, err := fetchIssueDetails(repo, issueNumber)
if err != nil {
fmt.Printf("Error fetching issue details: %v\n", err)
return
}
comments, err := fetchIssueComments(issue.CommentsURL)
if err != nil {
fmt.Printf("Error fetching issue comments: %v\n", err)
return
}
kbArticle, err := generateKBArticle(issue, comments)
if err != nil {
fmt.Printf("Error generating KB article: %v\n", err)
return
}
fmt.Printf(kbArticle)
err = postDiscussionToRepo(repo, fmt.Sprintf("Issue Summary: %s", issue.Title), kbArticle, token)
if err != nil {
fmt.Printf("❌ Error posting KB article as a discussion: %v\n", err)
return
}
fmt.Println("✅ KB article successfully posted as a discussion.")
}
How It Works:
- Calls
fetchIssueDetails()
to retrieve issue data from GitHub. - Calls
fetchIssueComments()
to fetch all comments on the issue. - Calls
generateKBArticle()
to create the KB article using an LLM. - Calls
postDiscussionToRepo()
to post the KB article as a GitHub Discussion. - If any function fails, it logs an error and stops execution.
Fetching Issue Details
To generate a useful KB article, we first need to fetch issue details from GitHub using the REST API.
This function retrieves key information such as the issue title, description, state, creator, and reactions. It does not require an API token for public repositories, but if the repository is private, authentication is required, and an API token must be included in the request headers.
This function takes two parameters, both passed in from IssueClosedHandler
:
repo
(string) – The full repository name (owner/repo
).issueNumber
(int) – The number of the issue to fetch.
func fetchIssueDetails(repo string, issueNumber int) (*GitHubIssue, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/issues/%d", repo, issueNumber)
options := &http.RequestOptions{
Method: "GET",
Headers: map[string]string{
"Content-Type": "application/json",
},
}
request := http.NewRequest(url, options)
response, err := http.Fetch(request)
if err != nil {
return nil, fmt.Errorf("error fetching issue details: %w", err)
}
if response.Status != 200 {
return nil, fmt.Errorf("unexpected status code: %d", response.Status)
}
var issue GitHubIssue
if err := json.Unmarshal(response.Body, &issue); err != nil {
return nil, fmt.Errorf("error parsing issue details: %w", err)
}
return &issue, nil
}
What It Does:
- Constructs a GitHub API URL to fetch issue details
- Makes a
GET
request to retrieve the issue - Unmarshals the response JSON into a
GitHubIssue
struct - Returns the structured issue data
Fetching Issue Comments
In addition to issue details, we need to fetch comments to capture the discussion history. This function is called inside IssueClosedHandler
and receives the commentsURL
parameter, which is returned from the fetchIssueDetails
function.
func fetchIssueComments(commentsURL string) ([]GitHubComment, error) {
options := &http.RequestOptions{
Method: "GET",
Headers: map[string]string{
"Content-Type": "application/json",
},
}
request := http.NewRequest(commentsURL, options)
response, err := http.Fetch(request)
if err != nil {
return nil, fmt.Errorf("error fetching issue comments: %w", err)
}
if response.Status != 200 {
return nil, fmt.Errorf("unexpected status code: %d", response.Status)
}
var comments []GitHubComment
if err := json.Unmarshal(response.Body, &comments); err != nil {
return nil, fmt.Errorf("error parsing comments: %w", err)
}
return comments, nil
}
What It Does:
- Receives the comments URL from
fetchIssueDetails
- Makes a
GET
request to fetch all comments - Unmarshals them into a
GitHubComment
slice and returns them
Generating the KB Article
With the issue details and comments retrieved, we now use Modus to call an LLM model and generate a structured markdown knowledge base article. This function constructs a detailed prompt with all relevant issue data and passes it to the model for processing.
func generateKBArticle(issue *GitHubIssue, comments []GitHubComment) (string, error) {
prompt := fmt.Sprintf(`
Generate a detailed markdown article summarizing the GitHub issue.
### Issue Details:
- **Title**: %s
- **Description**: %s
- **State**: %s
- **Created by**: %s
- **Created at**: %s
- **Labels**: %v
- **Reactions**: 👍 (%d), 👎 (%d), ❤️ (%d), 🎉 (%d), 🚀 (%d), 👀 (%d)
### Comments:
%s
Generate the output in markdown format.
`,
issue.Title, issue.Body, issue.State, issue.User.Login, issue.CreatedAt,
getLabelNames(issue.Labels),
issue.Reactions.PlusOne, issue.Reactions.MinusOne, issue.Reactions.Heart,
issue.Reactions.Hooray, issue.Reactions.Rocket, issue.Reactions.Eyes,
formatComments(comments),
)
model, err := models.GetModel[openai.ChatModel]("generate-article")
if err != nil {
return "", fmt.Errorf("error fetching model: %w", err)
}
input, err := model.CreateInput(
openai.NewSystemMessage("You are an assistant that generates markdown documentation."),
openai.NewUserMessage(prompt),
)
if err != nil {
return "", fmt.Errorf("error creating model input: %w", err)
}
input.Temperature = 0.7
output, err := model.Invoke(input)
if err != nil {
return "", fmt.Errorf("error invoking model: %w", err)
}
return strings.TrimSpace(output.Choices[0].Message.Content), nil
}
How It Works:
- Formats issue details and comments into a structured prompt using the helper functions
getLabelNames
andformatComments
. - Calls the LLM model using Modus to generate a KB article.
- Ensures the output is valid markdown for easy readability.
- Returns the formatted article for posting as a GitHub Discussion.
This function is the key step in transforming issue discussions into useful documentation using AI.
Posting the Discussion
Once the knowledge base article is generated, we need to create a new GitHub Discussion and post the article as its content.
This function takes in the repository name, discussion title, the KB article body, and a GitHub API token for authentication.
func postDiscussionToRepo(repo string, title string, body string, token string) error {
// Extract owner and repository name
parts := strings.Split(repo, "/")
if len(parts) != 2 {
return fmt.Errorf("invalid repository format: %s", repo)
}
owner, repoName := parts[0], parts[1]
// Fetch repository ID
repoID, err := getRepositoryID(owner, repoName, token)
if err != nil {
return fmt.Errorf("error fetching repository ID: %w", err)
}
// Fetch discussion category ID
categoryID, err := getDiscussionCategoryID(repoID, token)
if err != nil {
return fmt.Errorf("error fetching discussion category ID: %w", err)
}
// GraphQL mutation to create a discussion
createDiscussionMutation := `
mutation($repoID: ID!, $categoryID: ID!, $title: String!, $body: String!) {
createDiscussion(input: {repositoryId: $repoID, categoryId: $categoryID, title: $title, body: $body}) {
discussion {
url
}
}
}`
// Prepare variables for the mutation
variables := map[string]string{
"repoID": repoID,
"categoryID": categoryID,
"title": title,
"body": body,
}
payload := map[string]interface{}{
"query": createDiscussionMutation,
"variables": variables,
}
// Send the request
options := &http.RequestOptions{
Method: "POST",
Headers: map[string]string{
"Authorization": "Bearer " + token,
"Content-Type": "application/json",
},
Body: payload,
}
request := http.NewRequest("https://api.github.com/graphql", options)
response, err := http.Fetch(request)
if err != nil {
return fmt.Errorf("error creating discussion: %w", err)
}
if response.Status != 200 {
return fmt.Errorf("failed to create discussion, status: %d, body: %s", response.Status, string(response.Body))
}
fmt.Println("✅ Discussion created successfully.")
return nil
}
How It Works:
- Extracts repository owner and name from the
repo
parameter. - Calls
getRepositoryID()
to fetch the repository ID using GitHub's GraphQL API. - Calls
getDiscussionCategoryID()
to retrieve a discussion category ID. - Sends a GraphQL mutation request to create a discussion with the KB article.
- If the request succeeds, it logs a success message. Otherwise, it returns an error.
This function allows us to post our AI-generated article as a structured discussion, making it easier for teams to find and reference solutions based on closed issues.
Running Locally
Before deploying, you can test your application locally using the Modus local development server. Modus provides a built-in GraphQL API Explorer, which allows you to interact with your functions.
Steps to Test Locally:
-
Start the Local Modus Server
Make sure you have Modus installed and start the server by running:
modus dev
-
Open the API Explorer
Once the server is running, navigate to
http://localhost:8686/explorer
in your browser. -
Test the Exported Function
You will see
IssueClosedHandler
listed as an available function.- Click on
IssueClosedHandler
to expand it. - Enter a repository name (e.g.,
"owner/repo-name"
) and an issue number in order to test fetching issue details and generating a KB article. - If you want to post the KB article as a GitHub discussion, provide a GitHub API token as well.
- Click on
-
Run the Function
Click Run, and you should see:
- Issue details and comments fetched from GitHub
- A KB article generated using the LLM
- If a GitHub API token is provided, a new discussion will be created
Deploy to Hypermode
Now that your Modus application is ready, let's deploy it to Hypermode. This will enable it to run in production and be triggered by the GitHub Action when an issue is closed.
Create a Hypermode Project:
Hypermode manages deployments through projects. The recommended way to create a project is via the Hypermode Console.
-
Go to the Hypermode Console.
-
Click New Project and enter a name for your app.
-
Connect your GitHub repository to Hypermode:
- Using the Hypermode Console: Select your repository from the list.
- Using the CLI:
Install the CLI with:
Link your project with:npm install -g @hypermode/hyp-cli
hyp link
-
In your project, create a new workflow file:
.github/workflows/ci-modus-build.yml
-
Copy and paste the GitHub Action code provided in the Hypermode Console (Step 3).
-
Commit and push the changes to trigger a new deployment.
Once deployed, go to the Hypermode Dashboard to view:
- Your project's endpoint
- Configured models and functions
- Your Hypermode API key
Your application is now live and ready to handle GitHub issue events!
Adding the Issue Summarizer GitHub Action
To automatically summarize issues when they are closed, you’ll set up a GitHub Action that triggers your Modus function. This workflow listens for closed issues and sends a request to your Hypermode function.
In your repository, create a new workflow file:
.github/workflows/issue-summarizer.yml
Then, add the following YAML configuration:
name: Issue Summarizer
on:
issues:
types:
- closed
jobs:
trigger-hypermode:
runs-on: ubuntu-latest
permissions:
discussions: write
steps:
- name: Trigger Hypermode Function
run: |
curl -X POST https://YOUR_HYPERMODE_ENDPOINT.hypermode.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ secrets.HYPERMODE_API_KEY }}" \
-d '{
"query": "query { issueClosedHandler(repo: \"${{ github.repository }}\", issueNumber: ${{ github.event.issue.number }}, token: \"${{ secrets.GITHUB_TOKEN }}\") }"
}'
Be sure to replace "https://YOUR_HYPERMODE_ENDPOINT.hypermode.app/graphql" with your actual Hypermode endpoint.
When an issue is closed, the GitHub Action triggers a POST
request to your Modus function via the Hypermode GraphQL API. It sends three key parameters:
- The repository name from
${{ github.repository }}
- The issue number (
issueNumber
) from${{ github.event.issue.number }}
- The GitHub token (
token
) from${{ secrets.GITHUB_TOKEN }}
for authentication
To keep your credentials secure, the workflow uses GitHub Secrets instead of hardcoding sensitive values. Here are two terms you'll need to understand:
HYPERMODE_API_KEY
- API key for authenticating requests to Hypermode.GITHUB_TOKEN
- Automatically provided by GitHub for authentication when calling APIs.
You must add your HYPERMODE_API_KEY
to your repository's GitHub Secrets:
- Go to your GitHub repository.
- Navigate to Settings → Secrets and variables → Actions.
- Click New repository secret.
- Add
HYPERMODE_API_KEY
and paste your Hypermode API key. - Click Save.
With this setup, when an issue is closed, the GitHub Action will trigger your Modus function, which will use the GitHub API to gather information about the closed issue. Modus will then leverage an LLM to generate a knowledge base article based on this information and use the GitHub API again to create a new discussion, posting the generated article. Give it a try!
Conclusion
I hope you found this guide helpful in building a GitHub Issue Summarizer. If everything went smoothly, you now have an app that will automatically generate knowledge base articles and post them to GitHub Discussions.
This is just one of the many projects you can build using the Modus framework, and I encourage you further explore all that Hypermode has to offer.
What are you waiting for? Sign up for Hypermode and start building model-native apps today.