Fetching the latest release of a GitHub package with Cloudflare Workers
There's an example of this worker up at https://mod.jpwilliams.dev/github-release-version
that can be used for any package like so: https://mod.jpwilliams.dev/github-release-version/jpwilliams/midi-mixer-releases.
When creating the website for MIDI Mixer, I needed to be able to list the latest version available to download without making any hefty requests and without updating the site every time I updated the software.
GitHub provides a wonderful API that's exceedingly easy to use, meaning we can mock up a Cloudflare Worker that returns the data we need with not a huge amount of effort.
A Basic Worker
The simplest Worker we can produce is the one that Cloudflare provides as its default: a "Hello World" worker. It looks like this:
addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event.request));
});
/**
* Respond to the request
* @param {Request} request
*/
async function handleRequest(request) {
return new Response("hello world", { status: 200 });
}
We won't go in to Workers in-depth here, but they work very similarly (both in code and function) to a ServiceWorker. If you've never used Cloudflare Workers before, I'd strongly suggest checking out their Quick Start guide to get going, well, quickly.
For our purposes, we're first going to need to figure out how to to retrieve the data we need.
The GitHub API
GitHub allows you to explore pieces of their API with no credentials, meaning we can instantly see what information we can get. After a quick look on the GitHub docs site I found the Get the latest release endpoint. You can go and check out the response right now: https://api.github.com/repos/jpwilliams/midi-mixer-releases/releases/latest.
There's a lot of data there, but the key piece for us is the tag_name
property. The tags I use for versioning on GitHub are semver-compatible version strings prefixed with v
, so this is perfect to use as my version number.
Now we know what we're looking for, let's add a function to our worker that'll pull the data we need.
async function getVersion(username, repo) {
const targetUrl = `https://api.github.com/repos/${username}/${repo}/releases/latest`;
const headers = new Headers();
headers.set("accept", "application/vnd.github.v3+json");
headers.set("authorization", "token YOUR_GITHUB_TOKEN");
headers.set("user-agent", "YOUR_GITHUB_USERNAME");
try {
const res = await fetch(targetUrl, { headers });
const release = await res.json();
if (!release)
throw new Error(`No valid releases found for ${username}/${repo}`);
return new Response(release.tag_name || "");
} catch (err) {
console.log(err);
return new Response("");
}
}
getVersion
is a function that takes a username
and a repo
, and returns a response containing either the version string if it was successful or nothing (but still a 200 OK
status) if it wasn't. You should handle failing cases depending on your use, but for me it made sense to just "silently" fail and return nothing.
There are a few pieces to pick apart here.
First, the headers. When calling the GitHub API from your browser, it realises this and lets you through (with a tight rate limit). When accessing it programmatically, however, GitHub needs some more love. For this to work you'll need to:
- Set
Accept
toapplication/vnd.github.v3+json
. This tells GitHub that you're intending to send an API request and are ready for the format it's delivered in. - Set
Authorization
totoken YOUR_GITHUB_TOKEN
. You can generate a token on your Personal Access Tokens page. It'll need either therepo
orpublic_repo
permission depending on your needs. - Set
User-Agent
to your username. GitHub actually requires this as a means to provide accountability for particular API calls.
Now we have our main code, we'll need to expand our "Hello World" handleRequest
to a much nicer version.
Handling the request
handleRequest
needs to grab the data that we need to be able to send to getVersion
. In this Worker, I've done this using the URL path. We can access the path really easily by using the URL
interface with the string URL we get in the request.
async function handleRequest(request) {
const url = new URL(request.url);
const [username, repo] = url.pathname.slice(1).split("/").slice(-2);
if (!username || !repo) {
new Response(
"Invalid request. No username or repository specified. Format is /username/repo",
{
status: 400,
}
);
}
try {
const res = await getVersion(username, repo);
return res;
} catch (err) {
console.log(err);
return new Response("");
}
}
Awesome! We use a try
/catch
here to ensure that if anything goes wrong it's just logged and we return a silent failure. The only piece of code that might be a bit odd is how we extract username
and repo
.
Remember that a URL to access the current service I've deployed looks like this: https://mod.jpwilliams.dev/github-release-version/jpwilliams/midi-mixer-releases
. If a request comes in to that URL, here's what happens to extract username
and repo
:
// Request to: https://mod.jpwilliams.dev/github-release-version/jpwilliams/midi-mixer-release
const url = new URL(request.url);
console.log(url.pathname); // /github-release-version/jpwilliams/midi-mixer-releases
url.pathname
.slice(1) // github-release-version/jpwilliams/midi-mixer-releases
.split("/") // ["github-release-version", "jpwilliams", "midi-mixer-releases"]
.slice(-2); // ["jpwilliams", "midi-mixer-releases"]
That, along with destructuring gets us our values regardless of the base path (github-release-version
in this case) it's deployed on!
Sorted.
With that, we're done! A really quick, really clean method of grabbing the plain-text version number of the latest release of any GitHub repository.
If you're using this in production (or with Wrangler), please don't put your GitHub API token in the code. Instead, use Secrets. For an example of this, see the repo below.
The complete code base for this with TypeScript and Wrangler included are available at jpwilliams/github-release-version. Check out MIDI Mixer too if you fancy seeing a live implementation!