Auto-updating GitHub profile with GitHub actions

Auto-updating GitHub profile with GitHub actions

Oxida(c)tion

GitHub Actions is a powerful automation tool that enables developers to automate various software development workflows, such as building, testing, and deploying code. We won't be doing all that, in this tutorial, we'll demonstrate how to create a Github Action that updates your profile README every day at a specified time using cron. If you just want to use the action here's the link: Blog_It. Let's get started.

Rust App

We could have used any language but we're going to use Rust 🦀 (why? because it's awesome). Let's first create a CLI rust application that will fetch posts from the RSS feed and update a file. We'll take all the arguments from the user using clap.

use chrono::DateTime;
use clap::Parser;
use regex::Regex;
use reqwest::blocking::Client;
use rss::Channel;

/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// RSS url to fetch blogs from
    #[arg(short, long)]
    url: String,

    /// Number of times to greet
    #[arg(short, long, default_value_t = 5)]
    max_number_of_posts: u8,

    #[arg(short, long)]
    target_md: String
}

fn main() {
    let args = Args::parse();
    let url = args.url;
    let max_number_of_posts = args.max_number_of_posts;
    let target_md = args.target_md;

    let client = Client::new();
    let response = client.get(url).send().unwrap();
    let rss = response.bytes().unwrap();
    let channel = Channel::read_from(&rss[..]).unwrap();

    let mut content = String::new();
    for (_index, item) in channel
        .items()
        .iter()
        .enumerate()
        .take(max_number_of_posts.into())
    {
        let pub_date = DateTime::parse_from_rfc2822(item.pub_date().unwrap())
            .unwrap()
            .format("%Y-%m-%d")
            .to_string();
        content.push_str(&format!(
            "[{}]({}) - {}\n\n",
            item.title().unwrap(),
            item.link().unwrap(),
            pub_date
        ));
    }

    let re = Regex::new(r"(?s)<!-- blogs starts -->.*<!-- blogs ends -->").unwrap();
    let readme = std::fs::read_to_string(target_md.clone()).unwrap();
    let new_readme = re.replace_all(
        &readme,
        &format!("<!-- blogs starts -->\n{}\n<!-- blogs ends -->", content),
    );
    std::fs::write(target_md, new_readme.as_bytes()).unwrap();
}

Workflows

We'll have two workflows, one to build our executable whenever there's a change and one to run it on our cron schedule.

Cron

Cron is a time-based job scheduler used in Unix-based systems. It allows you to run commands or scripts at specified times. In GitHub Actions, you can schedule actions using cron syntax. Check out the syntax:

* * * * *
- - - - -
| | | | |
| | | | ----- Day of week (0 - 7) (Sunday = both 0 and 7)
| | | ------- Month (1 - 12)
| | --------- Day of month (1 - 31)
| ----------- Hour (0 - 23)
------------- Minute (0 - 59)

If you're confused, don't worry, you'll know what it means soon enough.

name: Update Profile README Everyday

on:
  schedule:
    - cron:  '0 0 * * *'

jobs:
  update_readme:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout repository
      uses: actions/checkout@v2

    - name: Update README
      run: |
        echo "Updating profile README"
        # Command to update the README here

In this example, the cron syntax 0 0 * * * specifies that the workflow should run every day at midnight (UTC). similarly, 32 13 1 12 2 specifies that it should run at 13:32 on 1st day-of-month and Tuesday in December. This website is really helpful, crontab.guru.

To create the GitHub Action, we need to create a couple of new workflows in our GitHub repository. To do this, go to your repository, click on the Actions tab, and then click on the New workflow button.

Build Workflow

  • It'll run on every push on "main" branch, on every PR merge on "main" branch and if triggered manually (workflow_dispatch).

  • We'll checkout our repository.

  • Build an optimized executable binary.

  • Upload it as an artifact.

name: Build on every update

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]
  workflow_dispatch:


env:
  CARGO_TERM_COLOR: always

jobs:
  build_binary:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout repository
      uses: actions/checkout@v3

    - name: Build release
      run: cargo build --bin blog_puller --release --verbose

    - uses: actions/upload-artifact@v3
      with:
        name: blog_puller_bin
        path: target/release/blog_puller

Update Workflow

  • It'll run every day at 00:00 UTC and if triggered manually (workflow_dispatch).

  • We'll checkout our repository.

  • Download the executable binary that we upload as an artifact.

  • Add execute permissions.

  • Run the executable.

  • Check if there were any changes in our repo files, if so, commit changes in "README.md" and push.

name: Update Profile README Everyday

on:
  schedule:
    - cron:  '0 0 * * *'

  workflow_dispatch:

jobs:
  update_readme:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout repository
      uses: actions/checkout@v3

    - name: Download binary
      uses: dawidd6/action-download-artifact@v2
      with:
        workflow: build.yml
        name: blog_puller_bin

    - name: Add permissions
      run: chmod 777 blog_puller

    - name: Run
      run: ./blog_puller

    - name: Commit and push if README changed
      run: |-
        git diff
        git config --global user.email "actions@users.noreply.github.com"
        git config --global user.name "README-bot"
        git diff --quiet || (git add README.md && git commit -m "Updated README")
        git push

Results

Once the later workflow has been successfully executed. The blogs will be visible on the profile README between tags <!-- blogs starts --> and <!-- blogs ends --> . Here's the complete example repo.

To get notified when the action is released or for similar articles follow me on Twitter at AzanulZ or connect with me on LinkedIn, Azanul Haque.

Did you find this article valuable?

Support Azanul Haque by becoming a sponsor. Any amount is appreciated!