Skip to content

Commit

Permalink
Finished initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
gautamkrishnar committed Jul 21, 2020
1 parent c7356a8 commit c93d7ba
Show file tree
Hide file tree
Showing 13 changed files with 1,436 additions and 72 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,4 @@ dist
.yarn/install-state.gz
.pnp.*

tests/README.md
test/README.md
64 changes: 62 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,62 @@
# blog-post-workflow
Allows you to show your latest blog posts on your github profile or project readme.
# Blog post workflow
List your latest blog posts from different sources on your Github profile/project readme automatically using this github action:

![preview](https://user-images.githubusercontent.com/8397274/88047382-29b8b280-cb6f-11ea-9efb-2af2b10f3e0c.png)


### How to use
- Go to your repository
- Add the following section to your **README.md** file, you can give whatever title you want. Just make sure that you use `<!-- BLOG-POST-LIST:START --><!-- BLOG-POST-LIST:END -->` in your readme. The workflow will replace this comment with the actual blog post list:
```markdown
# Blog posts
<!-- BLOG-POST-LIST:START -->
<!-- BLOG-POST-LIST:END -->
```
- Create a folder named `.github` and create `workflows` folder inside it if it doesn't exist.
- Create a new file named `blog-post-workflow.yml` with the following contents inside the workflows folder:
```yaml
name: Latest blog post workflow
on:
push:
branches:
- master
workflow_dispatch:
schedule:
# Runs every hour
- cron: '0 * * * *'

jobs:
update-readme:
name: Update this repo's README with latest blog posts
runs-on: ubuntu-latest
steps:
- uses: gautamkrishnar/blog-post-workflow@master
with:
feed_list: "['https://dev.to/feed/gautamkrishnar', 'https://www.gautamkrishnar.com/feed/']"
```
- Replace the above url list with your own rss feed urls. See [popular-sources](#popular-sources) for a list of common RSS feed urls.
- Commit and wait for it to run
### Options
This workflow has additional options that you can use to customize it for your use case, following are the list of options available:
| Option | Default Value | Description | Required |
|--------|--------|--------|--------|
| `feed_list` | `[]` | List of feed urls formatted as javascript array, eg: `['https://example1.com', 'https://example2.com'`]` | Yes |
| `max_post_count` | `5` | Maximum number of posts you want to show on your readme, all feeds combined | No |
| `readme_path` | `./README.md` | Path of the readme file you want to update | No |
| `gh_token` | your github token with repo scope | Use this to configure the token of the user that commits the workflow result to GitHub | No |

### Popular Sources
Following are the list of some popular blogging platforms and their RSS feed urls:

| Name | Feed URL | Comments | Example |
|--------|--------|--------|--------|
| [Dev.to](https://dev.to/) | `https://dev.to/feed/username` | Replace username wih your own username | https://dev.to/feed/gautamkrishnar |
| [Wordpress](https://wordpress.org/) | `https://www.gautamkrishnar.com/feed/` | Replace wih your own blog url | n/a |

### Bugs
If you are experiencing any bugs, don’t forget to open a [new issue](https://github.com/gautamkrishnar/blog-post-workflow/issues/new).

### Liked it?
Hope you liked this project, don't forget to give it a star ⭐
14 changes: 5 additions & 9 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,18 @@ description: 'Allows you to show your latest blog posts on your github profile o
inputs:
gh_token:
description: 'GitHub access token with Repo scope'
required: true
default: ${{ github.token }}
USERNAME:
description: 'Your GitHub username'
default: ${{ github.repository_owner }}
required: false
README_PATH:
default: ${{ github.token }}
readme_path:
description: 'Path of the readme file you want to update'
default: './README.md'
required: false
MAX_POST_COUNT:
max_post_count:
description: 'Maximum number of posts you want to show on your readme, all feeds combined'
default: '5'
required: false
FEED_OBJECT:
description: "Javascript array that contains feed metadata, eg: [{url: 'https://site1.com/feeds', dateObj: 'date', itemsObj: 'items'}, url: 'https://site2.com/feeds', dateObj: 'date', itemsObj: 'items'}]",
feed_list:
description: "Javascript array that contains feed urls",
default: "[]"
required: true

Expand Down
190 changes: 140 additions & 50 deletions blog-post-workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,97 @@ const process = require('process');
let Parser = require('rss-parser');
const core = require('@actions/core');
const _ = require('lodash');
let parser = new Parser();
const fs = require('fs');
const { spawn } = require('child_process');

/**
* Builds the new readme by replacing the readme's <!-- BLOG-POST-LIST:START --><!-- BLOG-POST-LIST:END --> tags
* @param previousContent {string}: actual readme content
* @param newContent {string}: content to add
* @return {string}: content after combining previousContent and newContent
*/
const buildReadme = (previousContent, newContent) => {
const tagToLookFor = `<!-- BLOG-POST-LIST:`;
const closingTag = '-->';
const startOfOpeningTagIndex = previousContent.indexOf(
`${tagToLookFor}START`,
);
const endOfOpeningTagIndex = previousContent.indexOf(
closingTag,
startOfOpeningTagIndex,
);
const startOfClosingTagIndex = previousContent.indexOf(
`${tagToLookFor}END`,
endOfOpeningTagIndex,
);
if (
startOfOpeningTagIndex === -1 ||
endOfOpeningTagIndex === -1 ||
startOfClosingTagIndex === -1
) {
return previousContent
}
return [
previousContent.slice(0, endOfOpeningTagIndex + closingTag.length),
'\n',
newContent,
'\n',
previousContent.slice(startOfClosingTagIndex),
].join('');
};

/**
* Code to do git commit
* @return {Promise<void>}
*/
const commitReadme = async () => {
/**
* Executes a command
*/
const exec = (cmd, args = []) => new Promise((resolve, reject) => {
console.log(`Started: ${cmd} ${args.join(' ')}`);
const app = spawn(cmd, args, {stdio: 'inherit'});
app.on('close', (code) => {
if (code !== 0) {
err = new Error(`Invalid status code: ${code}`);
err.code = code;
return reject(err);
}
return resolve(code);
});
app.on('error', reject);
});

// Doing commit and push
await exec('git', [
'config',
'--global',
'user.email',
'[email protected]',
]);
await exec('git', ['config', '--global', 'user.name', 'blog-post-bot']);
await exec('git', ['add', README_FILE_PATH]);
await exec('git', ['commit', '-m', 'Updated with latest blog posts']);
await exec('git', ['push']);
};


// Blog workflow code

let parser = new Parser();
// Total no of posts to display on readme, all sources combined, default: 5
const TOTAL_POST_COUNT = Number.parseInt(core.getInput('MAX_POST_COUNT'));
const TOTAL_POST_COUNT = Number.parseInt(core.getInput('max_post_count'));
// Readme path, default: ./README.md
const README_FILE_PATH = core.getInput('README_PATH');
const README_FILE_PATH = core.getInput('readme_path');
const GITHUB_TOKEN = core.getInput('gh_token');
core.setSecret(GITHUB_TOKEN);

let POST_COUNT = 0; // Post counter
const promiseArray = []; // Runner
const runnerNameArray = []; // To show the error/success message
let postsArray = []; // Array to store posts
let jobFailFlag = false; // Job status flag

if(process.env.TEST) {
const data = [
{'url': 'https://gautamkrishnar.com/feed', 'itemsObj': 'items', 'dateObj': 'isoDate'},
{'url': 'https://dev.to/feed/gautamkrishnar', 'itemsObj': 'items', 'dateObj': 'isoDate'}
];
process.env.INPUT_MAX_POST_COUNT="5";
process.env.INPUT_FEED_OBJECT=JSON.stringify(data);
process.env.INPUT_README_PATH="./tests/README.md";
}

const feedObjString = core.getInput('FEED_OBJECT').trim();
const feedObjString = core.getInput('feed_list').trim();
let feedList = [];

// Reading feed list from the workflow input
Expand All @@ -40,39 +108,39 @@ if (!feedObjString || feedObjString === '[]') {
}
}

feedList.forEach((feedMeta)=> {
const siteUrl = _.get(feedMeta,'url');
if (siteUrl) {
promiseArray.push(new Promise((resolve, reject) => {
parser.parseURL(siteUrl).then((data) => {
const itemsKey = feedMeta.itemsObj ? feedMeta.itemsObj : 'items';
const dateKey = feedMeta.dateObj ? feedMeta.dateObj : 'isoDate';
const responsePosts = _.get(data, itemsKey);
if (responsePosts === undefined) {
reject("Cannot read response->" + itemsKey);
} else {
const posts = responsePosts.map((item) => {
if (item[dateKey] === undefined) {
reject("Cannot read response->" + itemsKey + "->" + dateKey)
}
return {
title: item.title,
url: item.link,
date: new Date(item[dateKey])
};
});
resolve(posts);
}
}).catch((e) => {
reject(e);
});
}));
runnerNameArray.push(siteUrl);
}
feedList.forEach((siteUrl) => {
promiseArray.push(new Promise((resolve, reject) => {
parser.parseURL(siteUrl).then((data) => {
const responsePosts = _.get(data, 'items');
if (responsePosts === undefined) {
reject("Cannot read response->item");
} else {
const posts = responsePosts.map((item) => {
// Validating keys to avoid errors
if (item['pubDate'] === undefined) {
reject("Cannot read response->item->pubDate");
}
if (item['title'] === undefined) {
reject("Cannot read response->item->title");
}
if (item['link'] === undefined) {
reject("Cannot read response->item->link");
}
return {
title: item.title,
url: item.link,
date: new Date(item.pubDate)
};
});
runnerNameArray.push(siteUrl);
resolve(posts);
}
}).catch(reject);
}));
});

// Processing the generated promises
Promise.allSettled(promiseArray).then((results) => {
let jobFailFlag = false;
results.forEach((result, index) => {
if (result.status === "fulfilled") {
// Succeeded
Expand All @@ -85,17 +153,39 @@ Promise.allSettled(promiseArray).then((results) => {
core.error(result.reason);
}
});
}).finally(() => {
// Sorting posts based on date
postsArray.sort(function(a,b){
postsArray.sort(function (a, b) {
return b.date - a.date;
});

// Slicing with the max count
postsArray = postsArray.slice(0, TOTAL_POST_COUNT);
if (postsArray.length > 0) {
console.log('Writing data to: ' + README_FILE_PATH);
try {
const readmeData = fs.readFileSync(README_FILE_PATH, "utf8");

const postListMarkdown = postsArray.reduce((acc, cur, index) => {
return acc + `- [${cur.title}](${cur.url})` + ((index === (postsArray.length - 1)) ? '' : '\n');
}, '');
const newReadme = buildReadme(readmeData, postListMarkdown);
// if there's change in readme file update it
if (newReadme !== readmeData) {
core.info('Writing to ' + README_FILE_PATH);
fs.writeFileSync(README_FILE_PATH, newReadme);
if (!process.env.TEST_MODE) {
commitReadme.then(()=> {
core.info("Readme updated successfully in the upstream repository");
// Making job fail if one of the source fails
jobFailFlag ? process.exit(1) : process.exit(0);
});
}
} else {
core.info('No change detected, skipping');
process.exit(0)
}
} catch (e) {
core.error(e);
process.exit(1);
}
}
// Making job fail if one of the source fails
jobFailFlag ? process.exit(1) : process.exit(0);
});
Loading

0 comments on commit c93d7ba

Please sign in to comment.