Easily restore your project to a previous version with our new Instant One-click Backup Recovery

Combining the GitHub API with Hygraph to create a changelog content model

In this article, we’ll take a look at using Content Federation concepts to create a changelog page for a GitHub repository that allows developers to work in GitHub and content editors to work in Hygraph and then merge those two data streams into one powerful API.
Bryan Robinson

Bryan Robinson

Feb 02, 2023
Combining the GitHub API with Hygraph to create a changelog content model

For developer-centric companies, changelogs are incredibly important. They provide important details to developers and are a great marketing opportunity. The reality is often a less-than-ideal workflow, however.

Developers write their release notes in GitHub — in Milestones, Pull Requests, and Commits — and the marketing team wants to work within a proven content management system.

This tug of war often is in the audience for the changelog, as well. Company leadership is looking for high-level information and a level of polish. Developers are looking for all the technical details of a new version.

This can feel like an insurmountable situation. The truth is, this split may actually be ideal. Speak differently to different audiences. Keep content where the teams are most comfortable working, and don’t duplicate effort.

In this article, we’ll take a look at using Content Federation concepts to create a changelog page for a GitHub repository that allows developers to work in GitHub and content editors to work in Hygraph and then merge those two data streams into one powerful API.

#Requirements

  • Basic GraphQL knowledge
  • Understanding of content modeling
  • Hygraph Account

#Content and development flow

The basic structure of what we’ll build is a content model that has the basic needs of marketing for a new version release of an open source product.

undefined

We, then, want to pair that with all the data available for new versions in GitHub.

undefined

The GitHub repository will use Milestones to tag various pull requests. As Developers work toward a new release, they’ll bundle various PRs into a single milestone. We’ll use that milestone name to join the two data groups together. Each milestone will have a name that follows semantic versioning principles (e.g. v0.1.0). This string will be used in a remote source field to pull that specific milestone’s PRs into the Hygraph API to make it accessible to our frontend.

Let’s dive into making this happen.

#Creating the general schema

We won’t cover the basics of content modeling in this post, but we want to create a content model for each release. Let’s take a shortcut and get this content model up and running by cloning this Hygraph project.

The cloned project comes with a Release model that already has a single piece of content ready for us to query. The basic structure of the schema is:

  • Title:String
  • Version: String
  • Slug: Slug
  • Body: Markdown (This could also be a Rich Text field)

The other bonus to using this project is that it comes with the very basics of a GitHub Remote Source set up. It’s not completely finished, though.

Setting up the GitHub GraphQL Remote Source

Let’s navigate to the new project’s Schema page and open the GitHub GraphQL Remote Source.

undefined

The Remote Source has the basic information needed to get started: an API URL, Source Type, name, and prefix. It needs one additional piece of information, a personal access token from GitHub. This will take the shape of an Authorization header in both the Headers area and the Introspection Headers area.

In order to use the GraphQL API for GitHub, you’ll need a “Classic” personal access token. This can be found in your account’s developer settings. It will need repository permissions and user read permissions.

undefined

Once we have the key — don’t forget to copy it! — we can add it to the Headers area for both the regular URL and the Introspection URL.

undefined

Once those fields are populated, we can create a new Remote Source field in our Release schema.

Setting up the Remote Source Field

Head over to the Release schema and from the list of fields choose a GraphQL field. We need some basic information on this field before we dive into the API itself.

We’ll call this the Pull Request Data, since we mostly want to get information on PRs. This will automatically generate an ID of pullRequestData which looks good. From there, the other defaults of the field are just fine.

Next, we need to dive into the API’s structure and create our Query.

We need to start at the Repository level. Select Repository from the list of input values. This will provide you with arguments to input and a list of information to output.

undefined

While we could at this point map our owner and name arguments to fields in our schema, let’s just choose a repository and put the owner’s username as owner and the repository’s name as the name. For my example, I’ll use a personal repository, but any repository that you can add milestones to will work:

  • Owner: brob
  • Name: plug11ty.com

From there, we can select the fields we want to get data from. In our case, we want to grab the Milestones. Much like Hygraph’s API, GitHub’s uses the Relay Connections Specification, so the Milestones will be paginated. We need to give it a number of Milestones to receive. For this example, use first and get the first 10 milestones. We won’t dive into pagination, but for a primer, check this article on Astro.js and Lazy Loading.

We also need to tell it which Milestone to show for each Release. We can’t hardcode this, so we’ll get an item from the model to populate this: {{ doc.version }}. This will grab the string that is input into the version field, and use that to query the GitHub API to find the milestone with a matching name.

Then, we can choose to surface a specific field to this query. In this case, choose the Nodes, as this will flatten the structure and get us to the PR data faster.

undefined

With that saved, we now have a remote source field. Our single release content already has a version added of 0.1.0 so we can head over to GitHub and add that Milestone to our repository.

#Preparing the GitHub repository

Hygraph is only one half of this equation. The GitHub repository needs to be formatted properly as well.

In whatever repository you chose to use, open up Milestones page. This can be found by clicking the “milestones” tab in either Issues or Pull Requests.

undefined

We can then add milestones with the New milestone button. The only requirement here is the title and the title should match the version inside Hygraph. If you’re using the demo content, that will be v.0.1.0.

Once the milestone is created, we need to add this milestone as data on a number of Pull Requests. Since this will be for a changelog, these should probably be “closed” PRs. Select a number of PRs and from the Milestones dropdown, bulk add our new milestone to the PRs.

undefined

That’s it! Now we can query this directly from our Hygraph API. Let’s head over and make a query in the API playground.

#Querying in the API Playground

Let’s take a look at what it will take to get to this data.

First, let’s get all the basic data from the Hygraph content.

query Release() {
releases {
slug
title
body
version
}
}

This gives us an array of releases with the data from the marketing team for each one.

Next we need to div into the Pull Requests for each. For that we can use the pullRequestData key we created with our field. This will have all the Milestone data including the PRs, title, and description. We only need the pullRequests. These are using the same pagination method as mentioned before, so we need to grab the first 10 PRs and list out the nodes on those to get the information.

Here we can get any information we want to display. Some good information would be the title, body, and URL. It might also be interesting to show how many lines of code were added or deleted. That information is all available in the API.

pullRequestData {
description
pullRequests(first: 10) {
nodes {
url
title
permalink
deletions
additions
body
}
}
}

The last thing to get is inside the PR’s nodes. We also want to get all the commits and display their information. The commits also are paginated, so be sure to provide the number you need and get the nodes. It also might be interesting to display the total number of commits, so grab the totalCount as well.

commits(first: 10) {
totalCount
nodes {
url
commit {
messageBody
messageHeadline
author {
name
}
}
}
}

With all this together, the query is rather large, but comprehensive.

query Releases {
releases {
body
slug
title
version
pullRequestData {
description
pullRequests(first: 10) {
nodes {
url
title
additions
deletions
body
commits(first: 10) {
totalCount
nodes {
url
commit {
messageBody
messageHeadline
author {
name
}
}
}
}
}
}
}
}
}

With that query, you can take this into any frontend framework you want and create amazing hybrids between marketing and developer content. Grab the following example code in Next.js and 11ty to give it a try.

#11ty Example

undefined

In 11ty, you’ll want to fetch this from a JavaScript data file and then use 11ty’s Pagination to create each release page.

// _data/releases.js
const EleventyFetch = require("@11ty/eleventy-fetch");
require('dotenv').config();
module.exports = async () => {
const query = `
query Releases {
releases {
body
slug
title
version
pullRequestData {
description
pullRequests(first: 10) {
nodes {
url
title
state
permalink
number
deletions
bodyText
body
additions
commits(first: 10) {
totalCount
nodes {
url
commit {
messageBody
messageHeadline
author {
name
}
}
}
}
}
}
}
}
}
`;
try {
const { data } = await EleventyFetch(`${process.env.HYGRAPH_ENDPOINT}`, {
fetchOptions: {
body: JSON.stringify({ query }),
method: "POST",
},
duration: '1s',
type: 'json',
verbose: true
})
const structured = data.releases.map(release => ({
...release,
pullRequestData: release.pullRequestData[0]?.pullRequests.nodes
}
))
return structured;
} catch (error) {
console.log(error);
}
};
---
# /release.html
pagination:
data: releases
size: 1
alias: release
permalink: releases/{{ release.slug }}/
layout: base.html
---
<h1 class="flex items-center gap-2"><span class="text-sm bg-transparent text-blue-900 font-semibold py-1 px-2 rounded-full border border-blue-900 ">{{ release.version }}</span> {{ release.title }}</h1>
{{ release.body | markdown }}
{% if release.pullRequestData %}
<h2 class="mt-10">Pull Requests</h2>
{% for pr in release.pullRequestData %}
<div class="pr mb-5 border-b-2 pb-5">
<h3><a href="{{ pr.url }}">{{ pr.title }}</a></h3>
<h4>(+{{pr.additions}}, -{{pr.deletions}} from {{pr.commits.totalCount}} Commits)</h4>
<p>{{ pr.bodyText}}</p>
<h5 class="text-lg font-bold">Commits</h5>
<ul class="my-0">
{% for commit in pr.commits.nodes %}
<li>By {{ commit.commit.author.name }} - <a href="{{ commit.url }}">{{ commit.commit.messageHeadline }} - {{
commit.commit.messageBody }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
{% endif %}

#Next.js Example

In Next.js, we’ll use dynamic routes to get the slug from the URL parameter and create props for each page from specific queries that pass that slug.

// /pages/releases/[slug].js
import styles from '../../styles/Home.module.css'
import Head from 'next/head'
const query = `
query Releases {
releases {
slug
title
}
}
`;
const fullQuery = `
query Release($slug: String!) {
release( where: {slug: $slug}) {
body
slug
title
version
pullRequestData {
description
pullRequests(first: 10) {
nodes {
url
title
state
permalink
number
deletions
bodyText
body
additions
commits(first: 10) {
totalCount
nodes {
url
commit {
messageBody
messageHeadline
author {
name
}
}
}
}
}
}
}
}
}
`
export async function getStaticPaths() {
const response = await fetch(process.env.HYGRAPH_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({query})
})
const data = await response.json()
const paths = data.data.releases.map((release) => ({
params: { slug: release.slug },
}))
return { paths, fallback: false }
}
export async function getStaticProps({ params }) {
const response = await fetch(process.env.HYGRAPH_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
query: fullQuery,
variables: { slug: params.slug },
}),
})
const data = await response.json()
return {
props: {
release: data.data.release,
},
}
}
export default function Release({release}) {
return (
<div className={styles.container}>
<Head>
<title>Changelog</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1>{release.title}</h1>
<div dangerouslySetInnerHTML={{__html: release.body}} />
<h2>Pull Requests</h2>
<ul>
{release.pullRequestData[0].pullRequests.nodes.map((pr) => (
<li key={pr.number}>
<h3><a href={pr.url}>{pr.title}</a></h3>
<p>{pr.bodyText}</p>
<h4>Commits</h4>
{pr?.commits?.nodes.map((commit) => (
<div key={commit.commit.messageHeadline}>
<h5><a href={commit.url}>{commit.commit.messageHeadline}</a></h5>
</div>
))}
</li>
))}
</ul>
</main>
</div>
)
}

#Summary

In this post, we combined two powerful APIs: GitHub’s GraphQL API and Hygraph’s Content API. We did that with a Remote Source that allows developers to do their commits and Pull Requests in GitHub and a Marketing team to use Hygraph. We merged those together with a Remote Source field in Hygraph and a Milestone title in GitHub. That Milestone was attached to a set of Pull Requests and those PRs were able to be pulled directly alongside the release notes from Marketing.

The best of both worlds is definitely possible when working toward a unified changelog.

Blog Author

Bryan Robinson

Bryan Robinson

Head of Developer Relations

Bryan is Hygraph's Head of Developer Relations. He has a strong passion for developer education and experience as well as decoupled architectures, frontend development, and clean design.