How to you use GitHub Issues as your CMS?
Rishi Raj Jain
Posted on November 1, 2023
In this tutorial, I'll guide you through the process of setting up GitHub Issues as your data source / CMS with Astro Endpoints to handle data operations. I used this method when I launched an open-source LinkTree Alternative, itsmy.fyi. I'll break down the code into steps to make it easier to follow 👇🏻
Setup GitHub Issues Webhook
To set up GitHub Issues as your data source and establish a connection to GitHub, follow these steps:
Step 1: Create a GitHub Repository
If you don't already have a GitHub repository for your project, create one on GitHub.
Step 2: Generate a GitHub Personal Access Token
To interact with GitHub's API, you'll need a personal access token with the necessary permissions. Follow these steps to create one:
- Go to your GitHub account settings.
- In the left sidebar, click on
Developer settings
. - Click on
Personal access tokens
, and thenGenerate token
. - Select the required scopes (typically, you'll need
repo
or specific repository access).
Generate the token and securely store it as GITHUB_API_TOKEN
.
Step 3: Create a GitHub Repo Webhook
- Under your repository name, click Settings. If you cannot see the
Settings
tab, select the dropdown menu, then click Settings. - In the left sidebar, click Webhooks.
- Click Add webhook.
- Under
Payload URL
, type the URL where you'd like to receive payloads at for example, https://itsmy.fyi/github/hook/issue. - Under
Secret
, type a string to use as a secret key. You should choose a random string of text with high entropy.
Store this secret as the environment variable named GITHUB_WEBHOOK_SECRET
.
Step 4: Configure GitHub API Authentication
In your Astro project, create a configuration file (e.g., .env) to store the GitHub token and webhook secret securely. Make sure to use environment variables (i.e. via import.meta.env.ENV_NAME
) to access the token in your code.
Enabling SSR in Your Astro Project
Modify your astro.config.mjs to have configuration as below (I used Vercel to deploy my Astro site) 👇🏻
// File: astro.config.mjs
import { defineConfig } from 'astro/config'
import vercel from '@astrojs/vercel/serverless'
export default defineConfig({
// ...
output: 'server',
adapter: vercel(),
// ...
})
Setup Octokit
The octokit package integrates the three main Octokit libraries:
(1) API client (REST API requests, GraphQL API queries, Authentication)
(2) App client (GitHub App & installations, Webhooks, OAuth)
(3) Action client (Pre-authenticated API client for single repository)
Out of which, we're solely making use of (1) right now.
// API Reference
// https://github.com/octokit/rest.js
import { Octokit } from '@octokit/rest'
const auth = import.meta.env.GITHUB_API_TOKEN
export const octokit = new Octokit({ auth })
Post Comments through Octokit
Using OctoKit createComment
function, respond to events related to GitHub Issues upon executing relevant actions:
// API Reference
// https://github.com/octokit/rest.js
export const createComment = async (context, body) => {
await octokit.rest.issues.createComment({
owner: context.repository.owner.login,
repo: context.repository.name,
issue_number: context.issue.number,
body,
})
}
Verify GitHub Request Signature
In the verifySignature
function, you need to ensure that the incoming GitHub webhook request's signature matches the expected signature to verify the request's authenticity.
Here's a code snippet that demonstrates how you can verify the GitHub request signature:
import { createHmac } from 'crypto'
export const verifySignature = (context, header) => {
const secret = import.meta.env.GITHUB_WEBHOOK_SECRET
const expectedSignature = `sha256=${createHmac('sha256', secret).update(context).digest('hex')}`
return header === expectedSignature
}
Create GitHub Webhook Handler Endpoint
The POST
function is the webhook handler that listens to GitHub events related to GitHub Issues. Depending on the event type (opened, edited, closed), you can perform different actions within this function.
Here's how you can handle the events:
// File: src/pages/github/hook/issue.ts
// Process rawBody from the request Object
async function getRawBody(request: Request) {
let chunks = []
let done = false
const reader = request.body.getReader()
while (!done) {
const { value, done: isDone } = await reader.read()
if (value) {
chunks.push(value)
}
done = isDone
}
const bodyData = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0))
let offset = 0
for (const chunk of chunks) {
bodyData.set(chunk, offset)
offset += chunk.length
}
return Buffer.from(bodyData)
}
export async function POST({ request }) {
// Parse the request body as JSON
const rawBody = await getRawBody(request)
// Convert the raw body to JSON
const context = JSON.parse(rawBody.toString())
// If not a suitable GitHub Issue event
if (!['closed', 'edited', 'opened'].includes(context.action.toLowerCase())) {
return new Response(null, 400)
}
// Verify the signature
if (!verifySignature(rawBody, request.headers.get('X-Hub-Signature-256'))) {
return new Response(null, 403)
}
if (context.action === 'opened') {
// do action when a new issue is opened
// await createComment(context, 'Thanks for creating a profile via itsmy.fyi')
return new Response(null, { status: 200 })
}
else if (context.action === 'edited') {
// do action when a new issue is edited
// await createComment(context, 'Thanks for editing your profile via itsmy.fyi')
}
else if (context.action === 'closed') {
// do actions when a new issue is closed
// await createComment(context, 'Thanks for using itsmy.fyi')
return new Response(null, { status: 200 })
}
}
You're Done!
In this tutorial, we've set up GitHub Issues as our data source, perform actions (such as creating comments) all automated via Astro endpoints.
Happy coding!
Posted on November 1, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.