DEV Community

Joan Roucoux
Joan Roucoux

Posted on • Edited on

Update README with Latest Articles Using GitHub Actions and Deno 2

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 using Deno 2.0 (with Typescript).

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 Deno with denoland/setup-deno@v2.
    3. Run the update-readme-articles.ts 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 Deno environment
        uses: denoland/setup-deno@v2
        with:
          deno-version: v2.x

      - name: Run script
        run: |
          deno run -A scripts/update-readme-articles.ts

      - 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

2.1 Adding markers

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

2.2 Initializing the Deno Project

Run the following command to initialize a Deno project (make sure Deno is installed locally. If not, follow this):

deno init
Enter fullscreen mode Exit fullscreen mode

2.3 Creating the script logic

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

  1. Create the required files:
    scripts/types.ts: define the types for your articles.
    scripts/update-readme-articles.ts: main logic to fetch and process articles.

  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 are the .ts files, which performs the above steps:

// types.ts
interface User {
  name: string;
  username: string;
  twitter_username: string | null;
  github_username: string;
  user_id: number;
  website_url: string;
  profile_image: string;
  profile_image_90: string;
}

export interface Article {
  type_of: string;
  id: number;
  title: string;
  description: string;
  readable_publish_date: string;
  slug: string;
  path: string;
  url: string;
  comments_count: number;
  public_reactions_count: number;
  collection_id: number | null;
  published_timestamp: string;
  positive_reactions_count: number;
  cover_image: string | null;
  social_image: string;
  canonical_url: string;
  created_at: string;
  edited_at: string;
  crossposted_at: string | null;
  published_at: string;
  last_comment_at: string;
  reading_time_minutes: number;
  tag_list: string[];
  tags: string;
  user: User;
}

export interface ArticlePreview {
  title: string;
  url: string;
}

// update-readme-articles.ts
import * as fs from 'node:fs/promises';
import path from 'node:path';
import { Article, ArticlePreview } from './types.ts';

const DEV_TO_API_BASE_URL = 'https://dev.to/api';
const USERNAME = 'joanroucoux';

const main = async (): Promise<void> => {
  // 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(USERNAME);

    // 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 (
  username: string,
  page: number = 1,
  perPage: number = 5
): Promise<ArticlePreview[]> => {
  const response = await fetch(
    `${DEV_TO_API_BASE_URL}/articles?username=${username}&page=${page}&per_page=${perPage}`
  );
  const data: Article[] = await response.json();
  return data?.map((article: Article) => ({
    title: article.title,
    url: article.url,
  }));
};

// Generate markdown from articles
const generateArticlesContent = (articles: ArticlePreview[]): string => {
  let markdown = '';

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

  return markdown;
};

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

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

// Save file
const saveFile = async (filePath: string, content: string): Promise<void> => {
  try {
    const absolutePath = path.resolve(import.meta.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

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 deno run -A --watch scripts/update-readme-articles.ts 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 💯