Yet Another GitHub Profile Generator

A number of the companies I’ve been interviewing with are using Guide to create an “interview guide” that they send to candidates. I really like this. It includes information about the interview topic, interviewer profiles, and relevant links to pages on the company website and/or blog.

For one company, an interviewer profile linked to that person’s GitHub profile, so I took a look. Instead of the standard profile, they have a custom thing with lots of fun stats about their activity on GitHub. Neat!

But how do I do that? It took me an embarrassingly long time to figure out what the GitHub feature for this was named. The docs call it “Managing your profile README”. Doing it is simple. If your account name is “githubuser”, you make a new repo called “githubuser”. Then any README.md in that repo will be displayed on your GitHub profile page.

I think this feature has existed for a long time but I somehow managed to miss it. I took a look at how the profile I linked to, Eric’s, is generated. It’s using the lowlighter/metrics GitHub Actions. Each plugin generates an infographic, and you control what is generated by which plugins you use in your GitHub Actions workflow. The actual generation uses GitHub’s API (or other services for a few plugins).

But while I like how it looks, I don’t like that it only generates images. This means that nothing is clickable, so you can’t link to recent PRs or repos with recent activity, for example.

Fortunately, there are many other options. A search for “github profile generator” finds many options. One of the most popular appears to be Rahul Jain’s. You fill out a web form and it gives you some markdown to turn into your profile.

But I don’t like this one either. The content it generates is essentially static. If you want to update it you need to go to the form and change things (though you can save your config between uses).

I wanted something that would regenerate regularly, and I wanted it to include whatever I wanted. I also wanted the output to be Markdown (without lots of images) so it could have clickable links. There was nothing else to do but to build yet another profile generator!

Does the world need another profile generator? No, it doesn’t. Did I do it anyway? Yes, I did.

You can see its output in my profile right now.

How It Works

I wrote it in Rust. Is Rust the best language for this? Yes, because I want to learn more Rust.

The code lives in the autarch/autarch repo itself.

I started with octorust, which uses GitHub’s REST API (the V3 API). REST APIs are usually quite easy to understand, even though you end up with a lot of API calls in many cases. However, at one point I found a weird bug in octorust where the Repos::list_languages method inexplicably returns a Result<i64> instead of the data it should return.

The octorust crate is entirely generated, and the generated code lives alongside the generator in the oxidecomputer/third-party-api-clients repo. That repo has issues disabled, so I couldn’t report this (though it does allow PRs).

In the meantime, I’d been looking at how lowlighter/metrics was implemented and saw that it was using GitHub’s GraphQL API (the V4 API). I find GraphQL more challenging to use than REST since you need to formulate queries from scratch, rather than using predefined REST endpoints that correspond to specific resources. But the big advantage of GraphQL is that you can get exactly the data you want across multiple resources in a single query.

I decided that this was a good opportunity to learn something new, so I took some queries from the lowlighter/metrics repo and started massaging them to give me what I wanted.

At first, I tried a single query to give me all the repos to which I had access via the User repositories field. But for some reason, this seemed to skip some repos in my houseabsolute organization. There’s a good chance this was user error on my part, but I eventually ended up simply doing one query for the user and one for the org.

I used the graphql_client crate to make these requests. At first, I used its macros to generate code at compile time from my queries. But the huge downside of this is that it doesn’t play very nicely with code completion. The generated structs are quite complex, and the only way for me to find out what fields they contained was to use cargo-expand. But that didn’t help with code completion.

Fortunately, the crate also includes a graphql-client binary that will generate the same code that the macro does. This was much better. I could open the code in Emacs to view the generated structs and modules, and code completion just worked.

There was still one unfortunate issue, which is that even though my queries for user repos and org repos are nearly identical, the generator produces different Rust types for the user’s list of repos versus the org’s. This means that my stats collection code needs to be repeated for each type.

There are a couple of ways I could fix it. One would be to create a trait that each type can implement. But traits are defined in terms of methods. That means I’d have to write methods for each field in common between the types that I wanted to access. I did try this, but then I ran into lifetime issues when trying to take references to data in these structs. I’m sure those issues are solvable, but I just wanted to get a profile up, not spend all my time solving lifetime issues!

The other way to deal with this would be to write a macro of my own for the stats collection code. But that would have the same code completion problems, and that feels way over-engineered. I think what would be ideal would be a way to tell the graphql-client codegen that these two types are the same type. But that’s a PR for another day.

The final piece to make it all work is a simple GitHub Actions workflow that runs the generator. It runs whenever I push to the master branch of the generator repo as well as running nightly. If the generated README.md changes, it commits the changes and pushes the commit to the repo.

GitHub will stop running scheduled workflows for a repo after 60 days of inactivity, but I’m fairly sure that commits made by this workflow will count as activity, so it will never stop running. But I will find out in a couple of months.

If you want to use this for your own profile you’ll have to change some of the code. I have my username and org as a constant in the code, and most people probably don’t have a single org for most of their code. But really, you should probably use one of the many more battle-tested alternatives.