The one where you automatically add your latest posts to your GitHub profile
Take advantage of Hashnode's Public API
A few weeks ago I was taking a look at the list of posts featured on Hashnode when suddenly I saw one that immediately caught my eye:
In that post Sandro, member of the Hashnode Engineering team, lists up to 6 examples of what you can build taking advantage of their public GraphQL API.
I'm gonna talk here about the last example, because I really liked the idea of automatically adding my latest blog posts to my GitHub profile.
Jannik Wempe's profile is shown as an example of what we can achieve with the provided API and GitHub Actions.
No code is included in the post regarding this functionality, so I decided to investigate a bit further to implement it in my own profile.
GitHub profile
I started by reading the linked post by the people from GitHub, about how to manage the content of your profile.
Basically it says that you can include whatever information you want, as long as you meet prerequisites needed for it to be displayed:
- You've created a repository with a name that matches your GitHub username.
- The repository is public.
- The repository contains a file named
README.md
in its root. - The
README.md
file contains any content.
Follow the instructions included in that post to properly create the required repository and once it's created you can see a banner in the sidebar with following message:
Jannik uses a few GitHub Actions to include his latest blog posts and his recent activity in his profile. So did I.
I'll tell you the steps I followed to make all of this work.
Recent activity
After cloning your new repository, add .github/workflows/update-recept-activity.yml
file with following content:
name: Update Recent Activity
on:
schedule:
# 8:00 and 20:00 every day
- cron: '0 8,20 * * *'
workflow_dispatch:
jobs:
generate-feed:
runs-on: ubuntu-latest
name: Update Activity
steps:
- uses: actions/checkout@v4
# Pin the version to the latest stable version.
# If you want to live on edge, use 'master' branch.
- uses: recaptime-dev/github-activity-readme@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
A lot of stuff is going on here, so let me explain it:
- The workflow is scheduled to run every day at 8:00 and 20:00.
- The workflow is enabled to be triggered manually.
- The workflow is able to access to the repository using Checkout action.
- The workflow updates the
README.md
file with the recent GitHub activity using GitHub Activity in Readme action.
At the start of each workflow job, GitHub automatically creates a unique GITHUB_TOKEN
secret that you can use to authenticate on behalf of GitHub Actions. Check more details here.
Next you can see some screenshots for this workflow:
Manual dispatch enabled in the UI
Job manually triggered
Job triggered via schedule
Job successfully executed
Latest blog posts
Add .github/workflows/update-latest-blog-posts.yml
file with following content:
name: Update Latest Blog Posts
on:
workflow_dispatch:
# for trigger via webhooks
repository_dispatch:
types: [trigger]
jobs:
update-posts:
runs-on: ubuntu-latest
name: Update Posts
steps:
- uses: actions/checkout@v4
- uses: backpackerhh/github-latest-hashnode-posts@main
with:
HASHNODE_PUBLICATION_ID: ${{ secrets.HASHNODE_PUBLICATION_ID }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Once again a lot of stuff is going on here, so let me explain it:
- The workflow is enabled to be triggered manually.
- The workflow is enabled to be triggered with a webhook event, specifiying
trigger
as event type. - The workflow is able to access to the repository using Checkout action.
- The workflow updates the
README.md
file with my latest Hashnode blog posts using backpackerhh/github-latest-hashnode-posts action. I'll talk more about it later.
Again, GITHUB_TOKEN
is used to authenticate on behalf of GitHub Actions. A custom repository secret containing the Hashnode publication ID (HASHNODE_PUBLICATION_ID
) is used as well.
That particular value doesn't have to be secret, so you could directly put it in the YAML file if it suits you better.
Either way, you can add a new repository secret in following URL:
https://github.com/<username>/<username>/settings/secrets/actions/new
Next you can see some screenshots for this workflow:
Manual dispatch enabled in the UI
Job manually triggered
Job triggered via repository dispatch
Job successfully executed
Add content dynamically
For all of the above to work, following content must be initially added to the README.md
file:
## Latest Blog Posts
<!-- HASHNODE_POSTS:START -->
<!-- HASHNODE_POSTS:END -->
## Recent GitHub Activity
<!--START_SECTION:activity-->
<!--END_SECTION:activity-->
Those pairs of comments are used as placeholders so the GitHub actions can dynamically include the content between them.
Notice that a commit is created every time the content changes.
Settings
You can configure actions permissions in following URL:
https://github.com/<username>/<username>/settings/actions
You have to enable "Allow all actions and reusable workflows" permission (default) and grant "Read and write permissions" for workflows.
Custom GitHub action
This new repository is simply a fork from the custom action created by Jannik.
I preferred to create a fork instead of using his action because I wanted to customize some things.
Next, I'll talk about the more relevant files present in the repository.
package.json
In case you don't know anything about this file, check the official documentation for a detailed explanation about every option defined in it.
In summary, this file allows a package to make it easy for others to manage and install. Besides, you can list the packages your project depends on and the scripts that are available to be executed.
Force npm as package manager:
$ npm run preinstall
> github-latest-hashnode-posts@0.0.1 preinstall
> npx only-allow npm
Compile the action code:
$ npm run prepare
> github-latest-hashnode-posts@0.0.1 prepare
> ncc build src/index.js -o dist --source-map --license licenses.txt
ncc: Version 0.36.1
ncc: Compiling file index.js into CJS
9kB dist/licenses.txt
40kB dist/sourcemap-register.js
936kB dist/index.js
1092kB dist/index.js.map
1092kB dist/index.js.map
2077kB [2108ms] - ncc 0.36.1
This step is really important, because the content within dist
directory is the actual code that will run the GitHub action.
action.yml
Here is where the configuration and the metadata of the GitHub action is actually defined. For more details about all available options, check the official documentation.
For this specific action, I'd like to highlight a couple of them:
inputs
: A list of input parameters that the action accepts, such asGITHUB_TOKEN
,HASHNODE_PUBLICATION_ID
orMAX_POSTS
.runs
: Describes how the action should be executed, including the runtime environment (node20
) and the entry point (dist/index.js
).
src/index.js
In this file is where all the action happens (pun intended).
A request to the Hashnode GraphQL API is made with the inputs previously defined in action.yml
file:
query LatestPosts($id: ObjectId!, $first: Int!) {
publication(id: $id) {
id
posts(first: $first) {
edges {
node {
id
title
brief
publishedAt
url
coverImage {
url
}
}
}
}
}
}
Where id
is the Hashnode publication ID and first
is the number of posts that should be retrieved.
Hashnode provides a playground in case you need to customize your query. For instance, regarding what Jannik had, I added the url
of the post to the query so in my profile each post includes a link in the title and the cover.
A function gives then expected format to all the posts retrieved from the API and replaces the content of the README.md
file with them.
As a reminder, if everything goes well a commit is created with those changes.
The missing piece
There is a big difference regarding how the two workflows mentioned above run to update my profile. While I'm ok running one workflow twice a day to check any recent activity, the other one should only run when an event related to a blog post is triggered, either because it was published, updated or deleted.
Knowing that I'd need to automate that process somehow, the first thing that came to my mind was the webhooks provided by Hashnode:
However, I'd need to provide a URL to some kind of serverless function that would validate the secret defined in Hashnode (optional, but recommended) and trigger a request to the desired repository in GitHub.
There are a lot of services out there that would allow to do it very easily, such as AWS Lambda, Vercel or Netlify, to name a few, but it feels kind of overkill to set up something like that for what I want to achieve.
Bear in mind that I didn't check that approach, so I might be overlooking something else.
For that reason, I thought that it could be easier to just trigger an action from another repository.
Hashnode provides an integration with GitHub, so all my posts are backed up automatically to this repository:
After some research, I found this article which proved to be an invaluable help.
Let's see how it's done.
Add .github/workflows/dispatcher.yml
file to the repository where blog posts are backed up with following content:
name: Dispatcher
on:
push:
branches: [main]
jobs:
dispatch_event:
name: Dispatch event
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Dispatch
run: |
curl -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.DISPATCH_TOKEN }}" \
https://api.github.com/repos/backpackerhh/backpackerhh/dispatches \
-d '{"event_type":"trigger"}'
Yet once again a lot of stuff is going on here, so let me explain it:
- The workflow is enabled to be triggered when a push to main branch occurs.
- The workflow makes a POST request to the GitHub endpoint to create a repository dispatch event:
- The value of
event_type
is the one defined in the GitHub action of the target repository. - The
client_payload
parameter is available for any extra information that your workflow might need.
- The value of
Again, a custom repository secret is required to be used as authorization token (DISPATCH_TOKEN
).
You can add a new repository secret in following URL:
https://github.com/<username>/<blog_posts_repository>/settings/secrets/actions/new
The value of that secret must be the value of a personal access token (PAT) associated to your account.
If you prefer to create a classic PAT follow next steps:
- Go to github.com/settings/tokens/new
- Fill in following fields:
Note
-> Blog postsExpiration
-> No expiration (at your own peril)Select scopes
-> workflow (will mark repo automatically)
- Click on
Generate token
button
If you prefer to create a fine-grained PAT follow next steps:
- Go to github.com/settings/personal-access-tokens/..
- Fill in following fields:
Token name
-> Blog postsExpiration
-> 90 days (custom is around 100 days)Repository access
-> Only select repositories -> select<username>/<username>
Permissions
-> Repository permissions- Contents -> Access: Read and write
- Metadata -> Access: Read access (automatically selected)
- Click on
Generate token
button
Although is less secure, for this particular use case I've created a classic PAT without expiration. I prefer to simply generate a token and just forget about it.
Next you can see some screenshots for this workflow:
Job triggered via push to main branch
Job successfully executed
It's worth mentioning that you could use an already available GitHub action instead, such as repository-dispatch.
Workflow
Next, you can see how the process of updating my GitHub profile with the latest blog posts works:
And the result of running that workflow would be something as follows:
Conclusion
There was a lot of trial and error to get everything to work correctly, especially due to permissions issues with workflows and PATs, but it's not a really complex process.
To be honest, I had never thought about having a GitHub profile with something more than pinned repositories, but seeing the potential of this kind of workflows, from now on I will keep thinking about how to improve it even more.
Thank you for reading and see you in the next one!