As a developer, it is essential to have a GitHub account and profile to showcase your work and skills. You can include information such as your introduction, current projects, skills, contributions, and more. However, you can also leverage the power of Hashnode to share your latest blogs and increase your visibility. By doing this, your followers can stay updated on your knowledge-sharing activities and learn from your expertise.
I felt the same and added my latest blogs to my GitHub profile. As we know, GitHub supports markdown for profile README, so I have manually added my latest five blogs to my profile. However, this process became tedious and repetitive because I had to manually update my profile README whenever I published a new blog.
I started looking into how I can automate this process and have the latest updated blogs on my profile. At the same time, Hashnode announced that they have a huge update in their APIs, and new APIs are publicly available. I knew that if I wanted to automate something on GitHub, GitHub Actions was the best solution.
I already knew what GitHub Actions are and how they work; however, I did not know how to build one. I was very curious to create my own GitHub Action and make it available to the community so that others could also use it. Therefore, I decided to invest my time in learning how to build my own GitHub Actions that would help me fetch the latest post from my Hashnode publication and then display it on my Profile README.
For those of you who don't know what GitHub Actions are, you can read my blog here.
One more reason to build something with Hashnode APIs is the Hashnode APIs Hackathon. In this article, I will discuss what Hashnode Blog GitHub action is, the problem it solves, its functionality, how I built it, the challenges I faced, how I solved them, and more.
What is Hashnode blog GitHub Action?
It's a GitHub action to fetch and display your latest blog from Hashnode in a visually pleasing manner. If you are writing on Hashnode (if you don't have a blog yet, set it up here) and want to showcase your latest blogs on your profile README, you can use the Hashnode Blog Action to achieve the same.
Configuring the Hashnode Blog GitHub Action is straightforward, and it supports the following configuration options.
You can create a GitHub workflow as shown below to execute the Hashnode Blog Action.
You can replace blog.sachinchaurasiya.dev with your publication name, and you can also add the FORMAT, FILE, and POST_COUNT options. If you do not provide values for these options, the default values will take effect.
Examples of different available FORMAT options.
List
Stacked
Table
Card
Now that you understand the Hashnode Blog GitHub action, its functionality, and how it works, the upcoming section will delve into how I built it, the challenges I encountered, and how I resolved them.
How I Built the Hashnode Blog GitHub Action?
While learning about GitHub Actions, I came across the GitHub Actions Org, and they have a bunch of templates for building custom GitHub actions. So, I started searching for a template that has TypeScript support, ensuring type safety to write bug-free code. I found the typescript-action template that includes support for tests, linter, versioning, and more.
I clicked on the Use this template button, and within minutes, I had a basic GitHub action with TypeScript support. I then started adding code for my action logic.
First, I defined all the configuration and meta-information for my action in, including the name, description, author information, branding information, inputs, Node.js version, and entry point.
Then, I started working on logic to fetch the posts from Hashnode using a GraphQL query. I have not used any third-party library for querying Hashnode APIs, I just utilized the JavaScript Fetch API.
Then, I worked on utilities such as date and formats. Format utilities are designed to process the response from the Hashnode API posts and return the Markdown string for the requested output format.
For example, the piece of code below will process the list of posts and return the table Markdown format.
Last but not least, I worked on the entry point of the action, which is the main.ts file. This file includes logic for obtaining inputs, fetching posts, processing the posts, obtaining the output in markdown format, and then finding and replacing that content within a specified regex pattern. Finally, it commits and pushes the changes to the user file repository.
import*ascorefrom'@actions/core'import{fetchPosts}from'./hashnodeQuery'importfsfrom'fs'importcommitFilefrom'./commitFiles'import{getFormattedContent}from'./utils/formatUtils'import{ContentFormat}from'HashNodeTypes'constSECTION_REGEX=/^(<!--(?:\s|)HASHNODE_BLOG:(?:START|start)(?:\s|)-->)(?:\n|)([\s\S]*?)(?:\n|)(<!--(?:\s|)HASHNODE_BLOG:(?:END|end)(?:\s|)-->)$/gm/**
* The main function for the action.
* @returns {Promise<void>} Resolves when the action is complete.
*/exportasyncfunctionrun():Promise<void>{try{constpublicationName:string=core.getInput('HASHNODE_PUBLICATION_NAME')constpostCount:number=parseInt(core.getInput('POST_COUNT'))constoutputFileName:string=core.getInput('FILE')constformat:ContentFormat=(core.getInput('FORMAT')??'table')asContentFormatconstisDebug:boolean=core.getInput('DEBUG')==='true'// fetch posts from hashnodeconstresponse=awaitfetchPosts(publicationName,postCount)constposts=response.data.publication.posts.edges.map(edge=>edge.node)constfilePath=`${process.env.GITHUB_WORKSPACE}/${outputFileName}`constfileContent=fs.readFileSync(filePath,'utf8')constoutput=getFormattedContent(posts,format)constresult=fileContent.toString().replace(SECTION_REGEX,`$1\n${output}\n$3`)fs.writeFileSync(filePath,result,'utf8')// commit changes to the file when not in debug modeif (!isDebug){// eslint-disable-next-line github/no-thenawaitcommitFile().catch(err=>{core.error(err)core.info(err.stack)process.exit(err.code||-1)})}}catch (error){// Fail the workflow run if an error occursif (errorinstanceofError)core.setFailed(error.message)}}
Now that you have an understanding of how I built the action, let's delve into the challenges I faced in the next section.
Challenges Faced
The typescript-action template had a lot of pre-configured workflows. So, when I tried to build the action for the first time, it gave me a bunch of errors.
The second challenge was committing and pushing the changes to the user's file repository. This was, I would say, an exciting challenge, as it was my first time committing and pushing changes programmatically in GitHub.
I thought, No problem! I'm an engineer, and it's my job to face challenges and find solutions. Now, let me explain how I tackled these challenges.
How I Solved the Challenges
To overcome the initial challenge, I carefully read the guide in the template's README and adjusted or changed the workflows to fit my requirements. The key takeaway here is that reading the available documentation can be helpful.
The second challenge was a bit tricky, but I believed that every problem has a solution. I began by searching on Google to find out how to automatically commit and push changes to a GitHub repository. Unfortunately, I couldn't find helpful resources. So, I turned to ChatGPT and GitHub Copilot for assistance. They guided me towards using the GitHub Action bot config to commit and push changes. Check out the bash file for the details on how to do this for the user's file repository.
#!/bin/shset-eif[-z"$GITHUB_TOKEN"];then
echo"🚩 GITHUB_TOKEN Not Found. Please Set It As ENV Variable"exit 1
fi
git config --global user.email "githubactionbot+hashnode@gmail.com"
git config --global user.name "Hashnode Bot"DEST_FILE="${INPUT_FILE}"GIT_URL="https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git add "${GITHUB_WORKSPACE}/${DEST_FILE}"-fif["$(git status --porcelain)"!=""];then
git commit -m"📚 Latest Blog Updated"else
echo" ✅ Blog List Upto Date"fi
git push "$GIT_URL"
As this is not something that will be generated on every new build, I directly placed it in the dist directory.
Then, wrote a simple utility function to execute this Bash file and commit and push the changes.
import{spawn}from'child_process'importpathfrom'path'constexec=async (cmd:string,args:string[]=[]):Promise<number>=>newPromise((resolve,reject)=>{constapp=spawn(cmd,args,{stdio:'inherit'})app.on('close',(code:number)=>{if (code!==0){consterr=newError(`Invalid status code: ${code}`)Object.defineProperty(err,'code',{value:code})returnreject(err)}returnresolve(code)})app.on('error',reject)})constmain=async ():Promise<number>=>{returnawaitexec('bash',[path.join(__dirname,'./commit.sh')])}exportdefaultmain
This utility function uses the spawn function to execute bash commands.
Conclusion
I hope you find this article genuinely helpful, and that you've learned something from it. I've released the Hashnode blog GitHub action on the GitHub marketplace, so feel free to try it out and share your feedback. It's open source, so pull requests are welcome. You can report bugs, request features, and more.
Show your support for the project by giving it a star ⭐️.
Lastly, I want to express my gratitude to Hashnode for providing this fantastic opportunity to create something using Hashnode's brand-new set of APIs.
And that’s it for this topic. Thank you for reading.