How to you use GitHub Issues as your CMS?
Tags: webdev, javascript, tutorial, github
Published: 2023-11-01 09:51:26

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](https://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 then `Generate 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](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](https://vercel.com) to deploy my Astro site) 👇🏻

```javascript
// 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.

```typescript
// 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:

```typescript
// 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:

```typescript
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:

```typescript
// 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!