forked from gautamkrishnar/blog-post-workflow
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c7356a8
commit c93d7ba
Showing
13 changed files
with
1,436 additions
and
72 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -115,4 +115,4 @@ dist | |
.yarn/install-state.gz | ||
.pnp.* | ||
|
||
tests/README.md | ||
test/README.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ⭐ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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); | ||
}); |
Oops, something went wrong.