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:
Create a
.github/workflows/update_readme_articles.yml
file in your profile repository.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 theworkflow_dispatch
event trigger, enabling the workflow to be run manually when needed.-
Add in the
jobs
section the following steps:- Check out the codebase with
actions/checkout@v4
. - Set up Node.js with
actions/setup-node@v4
. - Run
update-readme-articles.js
script to update the README. - Commit and push only changed files to the repository.
- Check out the codebase with
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
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 ❤️
...
Next, you’ll need to create a script that follows these steps:
Create a
scripts/update-readme-articles.js
file in your profile repository.Read the original file content with
fs.readFile()
.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.
Generate new markdown with
generateArticlesContent()
and replace the content between markers withreplaceContentBetweenMarkers()
.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 -->
...
Which will be rendered as follows:
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();
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"
}
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.
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)
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 💯