DEV Community

Joan Roucoux
Joan Roucoux

Posted on • Updated on

Update README with Latest Articles Using GitHub Actions

Introduction

I wanted to give my GitHub profile a fresh new look, and since I’ve started writing articles on Dev.to, I thought it would be a great idea to automatically showcase the most recent ones on my profile.

You can find pre-made GitHub Actions workflows which periodically fetches the latest content and updates your README. However, I wanted to create a very basic, more flexible solution from scratch.

That’s why I’m writing this article, to share the steps with you, which you can easily adapt whether you’re fetching data from Dev.to, YouTube videos, RSS feeds, Medium, or any other source!

I’m assuming you already have a GitHub profile repository set up with a README.md file. If not, you can get started here.

1. Creating the GitHub Action Workflow

We can start by setting up the GitHub Action Workflow:

  1. Create a .github/workflows/update_readme_articles.yml file in your profile repository.

  2. Add a schedule to run the job every month at 0:00 UTC (or more often if you are a fast writer, which is not my case 😅). You can also include the workflow_dispatch event trigger, enabling the workflow to be run manually when needed.

  3. Add in the jobs section the following steps:

    1. Check out the codebase with actions/checkout@v4.
    2. Set up Node.js with actions/setup-node@v4.
    3. Run update-readme-articles.js script to update the README.
    4. Commit and push only changed files to the repository.
name: Update readme articles

on:
  # Add a schedule to run the job every month at 0:00 UTC
  schedule:
    - cron: '0 0 1 * *'

  # Allow running this workflow manually
  workflow_dispatch:

jobs:
  update-readme-articles:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout sources
        uses: actions/checkout@v4

      - name: Setup node environment
        uses: actions/setup-node@v4
        with:
          node-version: 'lts/*'

      - name: Run script
        run: |
          node scripts/update-readme-articles.js

      - name: Commit and push changes
        env:
          # This is necessary in order to push a commit to the main branch
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          git config --local user.email "github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git add .
          if [[ -n "$(git status --porcelain)" ]]; then
            git commit -m ":wrench: Update readme articles"
            git push origin main
          fi
Enter fullscreen mode Exit fullscreen mode

2. Updating the README

To update the README with the latest articles from Dev.to, start by adding markers to your README.md file as shown below:

# Hi 👋, I'm Joan Roucoux

## About me 💬

...

## My latest articles 📝

<!-- ARTICLES:START -->
<!-- ARTICLES:END -->

🚀 This section is updated by GitHub Actions Workflows ❤️

...
Enter fullscreen mode Exit fullscreen mode

Next, you’ll need to create a script that follows these steps:

  1. Create a scripts/update-readme-articles.js file in your profile repository.

  2. Read the original file content with fs.readFile().

  3. Fetch latest articles using the /articles operation from the Dev.to API with your name. For instance with mine, it gives https://dev.to/api/articles?username=joanroucoux&page=1&per_page=5. Feel free to adapt the source of your content here as mentioned in the introduction.

  4. Generate new markdown with generateArticlesContent() and replace the content between markers with replaceContentBetweenMarkers().

  5. Save the updated file with fs.writeFile().

The updated markdown code will look like this:

...
<!-- ARTICLES:START -->
- [Update README with Latest Articles Using GitHub Actions](https://dev.to/joanroucoux/update-readme-with-latest-articles-using-github-actions-41m3)
- [Building a Marvel Search Application with Qwik](https://dev.to/joanroucoux/building-a-marvel-search-application-with-qwik-ll7)
<!-- ARTICLES:END -->
...
Enter fullscreen mode Exit fullscreen mode

Which will be rendered as follows:

Updated markdown

Here is the script, which performs the above steps:

import { promises as fs } from 'fs';
import path from 'path';

// Get __dirname equivalent in ES modules
const __dirname = import.meta.dirname;

const main = async () => {
  // Read the original file content
  const filePath = '../README.md';
  const markdown = await readFile(filePath);

  // Proceed only if the file was read successfully
  if (markdown) {
    // Fetch latest articles
    const articles = await fetchArticles();

    // Generate new content
    const newContent = generateArticlesContent(articles);

    // Replace content between markers
    const START_MARKER = '<!-- ARTICLES:START -->';
    const END_MARKER = '<!-- ARTICLES:END -->';
    const updatedMarkdown = replaceContentBetweenMarkers(
      markdown,
      START_MARKER,
      END_MARKER,
      newContent
    );

    // Save the updated file
    await saveFile(filePath, updatedMarkdown);
  }
};

// Fetch latest articles
const fetchArticles = async () => {
  const response = await fetch(
    'https://dev.to/api/articles?username=joanroucoux&page=1&per_page=5'
  );
  const data = await response.json();
  return data?.map((article) => ({
    title: article.title,
    url: article.url,
  }));
};

// Generate markdown from articles
const generateArticlesContent = (articles) => {
  let markdown = '';

  articles?.forEach((article) => {
    markdown += `- [${article.title}](${article.url})\n`;
  });

  return markdown;
};

// Read file
const readFile = async (filePath) => {
  try {
    const absolutePath = path.resolve(__dirname, filePath);
    console.log('Reading file from:', absolutePath);
    const data = await fs.readFile(absolutePath, 'utf8');
    return data;
  } catch (err) {
    console.error('Error reading file:', err);
    return null;
  }
};

// Generate updated markdown
const replaceContentBetweenMarkers = (
  markdown,
  startMarker,
  endMarker,
  newContent
) => {
  const regex = new RegExp(`(${startMarker})([\\s\\S]*?)(${endMarker})`, 'g');
  const updatedMarkdown = markdown.replace(regex, `$1\n${newContent}$3`);
  return updatedMarkdown;
};

// Save file
const saveFile = async (filePath, content) => {
  try {
    const absolutePath = path.resolve(__dirname, filePath);
    await fs.writeFile(absolutePath, content, 'utf8');
    console.log('File has been saved successfully!');
  } catch (err) {
    console.error('Error saving file:', err);
  }
};

main();
Enter fullscreen mode Exit fullscreen mode

Because I'm using ES modules, don't forget to add a package.json at the root of your repository with the following content:

{
  "type": "module"
}
Enter fullscreen mode Exit fullscreen mode

3. Pushing and testing

That’s it! After pushing all the files to your repository, you can manually trigger the workflow from the "Actions" tab or wait for it to run based on the scheduled trigger. You can also test the script locally by running node update-readme-articles.js before pushing. Once completed, check the logs for any errors and verify that your README.md has been updated correctly.

GitHub Action Workflow

4. Conclusion

By automating the process of updating your README with GitHub Actions, you make sure the file stays up-to-date with your latest content. It's also a flexible solution that you can easily tweak to integrate different sources of data!

I hope you found this tutorial helpful! You can check out my repository here 🚀

Resources:

Top comments (1)

Collapse
 
khawla_chabchoub_f92f2bb2 profile image
Khawla Chabchoub

This is awesome 🚀🚀! I love how you’ve streamlined the process with GitHub Actions.
Keep up the great work.
Can’t wait to see what you come up with next 💯