Raywon Kari - Published on July 29, 2020 - 7 min read
GitHub silently released a new feature recently, wherein, one needs to create a new public repo matching the name of their username, and add a README.md file. GitHub will automatically render the README content in the overview page, accessible at https://github.com/USERNAME. After I created a repo with my username, GH shows the following:
This way, we can easily write stuff about ourself, share updates and many more. The scope of this feature is very broad. In this blog post, I want to share what I have done using this profile feature.
We will see the following aspects:
Basically, we will fetch the latest articles from my blog and render them in the README file, along with some info on my twitter handle and strava profile. Here is how the end result looks like:
We will also implement fetching the blog post data automatically and create pull requests if there are new updates, with the help of GitHub Actions. I am using golang to implement this project.
Here is an overview of the project:
In short, GitHub Actions is the CI/CD tool provided by GitHub out of the box. It helps to build, test, publish & deploy applications directly from GitHub without the need to implement and host other tools. It is tightly integrated to GitHub eco-system such as with pull requests, branches, issues, releases etc. GH actions is also used to automate lot of GH tasks, such as cleaning up, automations etc. Therefore it is a very powerful tool due to that it is tightly integrated to GitHub eco-system. In this project, we are also using GH Actions.
As I stated earlier, we are creating README file in the profile repo, which is rendered as-is in the overview page. Here we are using GH Actions to run the go code, which will generate the README file, and if there is any git diff, GH actions will create a PR to our master branch. We will then quickly review, and merge it so the README is updated.
So this is kind of a self-service auto-update README project, except the PR and merging process. We will also add a cron schedule so that, GH actions will run once everyday for checking updates.
We could easily implement auto-update as well, I just felt it to be not necessary for the moment, since I have some more work to do in this project, i.e., to fetch data from twitter, strava APIs, and embed them directly instead of the static data which is present currently during the time of the writing of this blog post.
To start using GH actions, all we need is to create a .github/workflows
directory in the root directory, and add a yml
file.
GH actions requires a workflow and a workflow is defined by the yml
file.
mkdir .gitHubcd .github/mkdir workflowscd workflows/touch main.yml
Once the yml file is created, we need to give the workflow a name, when the workflow should be executed, and what steps need to be performed. These are the bare minimum components we would need in order to use GH actions.
Here is my workflow file:
name: Build READMEon:push:branches: [ master ]pull_request:branches: [ master ]schedule:- cron: '0 8 * * *'jobs:build:name: Buildruns-on: ubuntu-lateststeps:- name: Set up Go 1.xuses: actions/setup-go@v2with:go-version: ^1.13id: go- name: Check out code into the Go module directoryuses: actions/checkout@v2- name: Runrun: go run buildreadme.go- name: Create Pull Requestuses: peter-evans/create-pull-request@v3.0.0
If we split it down:
name
is the name of the workflow.on
is the block which says when the workflow should be executedjobs
is the block which says what are the steps needs to be done, once the workflow is executed.As you can see in the jobs
section, we are using a GH managed agent to run our project i.e., ubuntu-latest
,
and the steps are simply setting up a go workspace to run our go code, we checkout the code, and run the go file.
Last component is the pull request component, which is an action created by the community.
Now lets dive into how we are generating README file using golang.
Firstly, I have separated out each section of the README file into its own package i.e., blog, twitter and strava into its own packages, and each package will generate its own README file, such as blog.md, twitter.md and strava.md.
From the main go code, I am simply executing these packages and appending all the individual README files into one.
Here is the directory structure:
.├── HOW_IT_WORKS.md├── LICENSE├── README.md├── blog # blog package│ └── blog.go├── blog.md # blog MD file├── buildreadme.go # MAIN GO CODE├── go.mod├── logo # logo's used in README│ ├── strava.png│ └── twitter.png├── strava # strava package│ └── strava.go├── strava.md # strava MD file├── twitter # twitter package│ └── twitter.go└── twitter.md # twitter MD file
In the blog package, I am sending a GET request to my blog's rss feed using the standard encoding/xml
available in go, and doing some minimalistic data processing to fetch the latest articles from my blog.
Here is an example of sending a GET request:
// Fetch data from RSS feed and decoderesp, err := http.Get(feedURL)decoder := xml.NewDecoder(resp.Body)
Once we have decoded, we will need a few variables which would hold the data type we need, such as Channels, Items etc. Here is an example of extracting the decoded data:
// typestype item struct {Title string `xml:"title"`Link string `xml:"link"`PubDate string `xml:"pubDate"`}type channel struct {Title string `xml:"title"`Link string `xml:"link"`Items []item `xml:"item"`}type rss struct {Channel channel `xml:"channel"`}// extractingerr = decoder.Decode(&data)if err != nil {fmt.Printf("Error Decode: %v\n", err)}// Now the 'data' variable holds the decoded data and can be accessed directly.// such as data.Channel data.Channel.Items etc// the responses are then written to a file named blog.md
Similar to blog package, we are generating twitter and strava md files, but they only hold static data, so it is straight forward to do so. Just create a file, and write the data to it.
We have created three packages, which when executed, will create respective MD files. In this main go code, we will write the code to execute these packages, and once they are done, we will append the data from the MD files, to the actual README.md file.
Let's execute the sub packages as follows:
// imported packagesimport ("github.com/raywonkari/raywonkari/blog""github.com/raywonkari/raywonkari/strava""github.com/raywonkari/raywonkari/twitter")// Trigger them in main function// generate twitter.mdfmt.Println("Generating Twitter MD File")twitter.Generate(twitterMdFile, twitterURL)// generate strava.mdfmt.Println("Generating Strava MD File")strava.Generate(stravaMdFile, stravaURL)// generate blog.mdfmt.Println("Generating Blog MD File")blog.Generate(blogMdFile, blogFeedURL)
Once this is done, it is again a straight forward task to append the content from different files into one file.
I used ioutil.ReadFile
to read the data from these different MD files, and used OpenFile
and WriteString
both from the os
package to write to the README file.
Full source code for this project can be found here and feel free to checkout the overview page here.
If you have any questions/thoughts/feedback, feel free to contact me in any platform you prefer.