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.
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.
RequirementsAnchor
- Basic GraphQL knowledge
- Understanding of content modeling
- Hygraph Account
Content and development flowAnchor
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.
We, then, want to pair that with all the data available for new versions in GitHub.
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 schemaAnchor
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 SourceAnchor
Let’s navigate to the new project’s Schema page and open the GitHub GraphQL Remote Source.
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.
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.
Once those fields are populated, we can create a new Remote Source field in our Release schema.
Setting up the Remote Source FieldAnchor
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.
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.
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 repositoryAnchor
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.
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.
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 PlaygroundAnchor
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 {slugtitlebodyversion}}
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 {descriptionpullRequests(first: 10) {nodes {urltitlepermalinkdeletionsadditionsbody}}}
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) {totalCountnodes {urlcommit {messageBodymessageHeadlineauthor {name}}}}
With all this together, the query is rather large, but comprehensive.
query Releases {releases {bodyslugtitleversionpullRequestData {descriptionpullRequests(first: 10) {nodes {urltitleadditionsdeletionsbodycommits(first: 10) {totalCountnodes {urlcommit {messageBodymessageHeadlineauthor {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 ExampleAnchor
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.jsconst EleventyFetch = require("@11ty/eleventy-fetch");require('dotenv').config();module.exports = async () => {const query = `query Releases {releases {bodyslugtitleversionpullRequestData {descriptionpullRequests(first: 10) {nodes {urltitlestatepermalinknumberdeletionsbodyTextbodyadditionscommits(first: 10) {totalCountnodes {urlcommit {messageBodymessageHeadlineauthor {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.htmlpagination:data: releasessize: 1alias: releasepermalink: 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 ExampleAnchor
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].jsimport styles from '../../styles/Home.module.css'import Head from 'next/head'const query = `query Releases {releases {slugtitle}}`;const fullQuery = `query Release($slug: String!) {release( where: {slug: $slug}) {bodyslugtitleversionpullRequestData {descriptionpullRequests(first: 10) {nodes {urltitlestatepermalinknumberdeletionsbodyTextbodyadditionscommits(first: 10) {totalCountnodes {urlcommit {messageBodymessageHeadlineauthor {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>)}
SummaryAnchor
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.