feat: generate awesome list from stars

This commit is contained in:
Simone Corsi
2021-01-15 10:43:56 +01:00
parent e88401859a
commit 0f6fa769c9
23 changed files with 34337 additions and 0 deletions

11
src/api.ts Normal file
View File

@@ -0,0 +1,11 @@
import got from 'got';
import core from '@actions/core';
const GITHUB_TOKEN = core.getInput('githubToken', { required: true });
export default got.extend({
headers: {
Authorization: `token ${GITHUB_TOKEN}`,
},
responseType: 'json',
});

100
src/git.ts Normal file
View File

@@ -0,0 +1,100 @@
// original content by: github.com/TriPSs/conventional-changelog-action/blob/master/src/helpers/git.js
import core from '@actions/core';
import exec from '@actions/exec';
const { GITHUB_REPOSITORY, GITHUB_REF } = process.env;
const IS_TEST = process.env.NODE_ENV === 'test';
const branch = GITHUB_REF?.replace('refs/heads/', '');
export default new (class Git {
commandsRun: string[] = [];
constructor() {
const githubToken = core.getInput('github-token', { required: true });
core.setSecret(githubToken);
const gitUserName = core.getInput('git-user-name');
const gitUserEmail = core.getInput('git-user-email');
// Set config
this.config('user.name', gitUserName);
this.config('user.email', gitUserEmail);
// Update the origin
this.updateOrigin(
`https://x-access-token:${githubToken}@github.com/${GITHUB_REPOSITORY}.git`
);
}
exec = (command: string): Promise<string> => {
return new Promise(async (resolve, reject) => {
if (IS_TEST) {
const fullCommand = `git ${command}`;
console.log(`Skipping "${fullCommand}" because of test env`);
if (!fullCommand.includes('git remote set-url origin')) {
this.commandsRun.push(fullCommand);
}
return resolve('done');
}
let execOutput = '';
const options = {
listeners: {
stdout: (data: Buffer) => {
execOutput += data.toString();
},
},
};
const exitCode = await exec.exec(`git ${command}`, undefined, options);
if (exitCode === 0) {
return resolve(execOutput);
} else {
return reject(`Command "git ${command}" exited with code ${exitCode}.`);
}
});
};
config = (prop: string, value: string) =>
this.exec(`config ${prop} "${value}"`);
add = (file: string) => this.exec(`add ${file}`);
commit = (message: string) => this.exec(`commit -m "${message}"`);
pull = async () => {
const args = ['pull'];
// Check if the repo is unshallow
if (await this.isShallow()) {
args.push('--unshallow');
}
args.push('--tags');
args.push(core.getInput('git-pull-method'));
return this.exec(args.join(' '));
};
push = () => this.exec(`push origin ${branch} --follow-tags`);
isShallow = async () => {
if (IS_TEST) return false;
const isShallow: string = await this.exec(
'rev-parse --is-shallow-repository'
);
return isShallow.trim().replace('\n', '') === 'true';
};
updateOrigin = (repo: string) => this.exec(`remote set-url origin ${repo}`);
createTag = (tag: string) => this.exec(`tag -a ${tag} -m "${tag}"`);
})();

143
src/index.ts Normal file
View File

@@ -0,0 +1,143 @@
import dotnenv from 'dotenv';
import fs from 'fs/promises';
import ejs from 'ejs';
import remark from 'remark';
import toc from 'remark-toc';
import GithubApi from './api';
import link from './link';
import git from './git';
import core from '@actions/core';
import type {
SortedLanguageList,
PaginationLink,
Stars,
Star,
ApiGetStarResponse,
} from './types';
const REPO_USERNAME = process.env.GITHUB_REPOSITORY?.split('/')[0];
const OUTPUT_FILENAME: string = core.getInput('output-filename') || 'README.md';
const IS_PROD = process.env.NODE_ENV === 'production';
const USERNAME = process.env.GITHUB_ACTOR || 'simonecorsi';
const API_STARRED_URL = `'https://api.github.com/users/${REPO_USERNAME}/starred'`;
const renderer = async (data: any) => {
try {
const MD_TEMPLATE = await fs.readFile('fixtures/template.md.ejs', 'utf-8');
return ejs.render(MD_TEMPLATE, data);
} catch (error) {
core.error(error);
return '';
}
};
if (!IS_PROD) {
dotnenv.config();
}
const wait = (time = 200) =>
new Promise((resolve) => setTimeout(resolve, time));
async function apiGetStar(url: string): Promise<ApiGetStarResponse> {
const { headers, body }: any = await (async () => {
if (!IS_PROD)
return JSON.parse(
await fs.readFile('fixtures/stars-response.json', 'utf-8')
);
return GithubApi.get(url);
})();
return {
data: body,
links: link.parse(headers.link).refs.reduce(
(acc, val) => ({
...acc,
[val.rel]: val.uri,
}),
{}
),
};
}
function isLastPage(links: PaginationLink): boolean {
return links.next === links.last;
}
export function generateMd(data: string): Promise<string> {
return new Promise((resolve) => {
remark()
.use(toc)
.process(data, function (error, file) {
if (error) {
core.error(error);
return resolve('');
}
return resolve(String(file));
});
});
}
export async function main(): Promise<any> {
let links: PaginationLink = {
next: API_STARRED_URL,
last: undefined,
};
let results: Stars = [];
do {
const r = await apiGetStar(links.next);
links = r.links;
results = results.concat(r.data);
if (!IS_PROD) break;
await wait();
} while (!isLastPage(links));
const sortedByLanguages = results.reduce(
(acc: SortedLanguageList, val: Star) => {
const language = val.language || 'generic';
if (!acc[language]) {
acc[language] = [val];
} else {
acc[language].push(val);
}
return acc;
},
{}
);
const rendered = await renderer({
username: USERNAME,
stars: Object.entries(sortedByLanguages),
updatedAt: Date.now(),
});
const markdown: string = await generateMd(rendered);
await fs.writeFile(OUTPUT_FILENAME, markdown);
await git.add(OUTPUT_FILENAME);
await git.commit(`chore(${OUTPUT_FILENAME}): updated list`);
await git.push();
}
export async function run(): Promise<any> {
try {
await main();
process.exit(0);
} catch (error) {
process.stderr.write(error);
process.exit(1);
}
}
const catchAll = (info: any) => {
core.error(info);
process.exit(1);
};
process.on('unhandledRejection', catchAll);
process.on('uncaughtException', catchAll);
run();

383
src/link.js Normal file
View File

@@ -0,0 +1,383 @@
/**
* Copyright (c) 2016 Jonas Hermsmeier
* https://github.com/jhermsmeier/node-http-link-header
*/
'use strict';
var COMPATIBLE_ENCODING_PATTERN = /^utf-?8|ascii|utf-?16-?le|ucs-?2|base-?64|latin-?1$/i;
var WS_TRIM_PATTERN = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;
var WS_CHAR_PATTERN = /\s|\uFEFF|\xA0/;
var WS_FOLD_PATTERN = /\r?\n[\x20\x09]+/g;
var DELIMITER_PATTERN = /[;,"]/;
var WS_DELIMITER_PATTERN = /[;,"]|\s/;
/**
* Token character pattern
* @type {RegExp}
* @see https://tools.ietf.org/html/rfc7230#section-3.2.6
*/
var TOKEN_PATTERN = /^[!#$%&'*+\-\.^_`|~\da-zA-Z]+$/;
var STATE = {
IDLE: 1 << 0,
URI: 1 << 1,
ATTR: 1 << 2,
};
function trim(value) {
return value.replace(WS_TRIM_PATTERN, '');
}
function hasWhitespace(value) {
return WS_CHAR_PATTERN.test(value);
}
function skipWhitespace(value, offset) {
while (hasWhitespace(value[offset])) {
offset++;
}
return offset;
}
function needsQuotes(value) {
return WS_DELIMITER_PATTERN.test(value) || !TOKEN_PATTERN.test(value);
}
class Link {
/**
* Link
* @constructor
* @param {String} [value]
* @returns {Link}
*/
constructor(value) {
/** @type {Array} URI references */
this.refs = [];
if (value) {
this.parse(value);
}
}
/**
* Get refs with given relation type
* @param {String} value
* @returns {Array<Object>}
*/
rel(value) {
var links = [];
var type = value.toLowerCase();
for (var i = 0; i < this.refs.length; i++) {
if (this.refs[i].rel.toLowerCase() === type) {
links.push(this.refs[i]);
}
}
return links;
}
/**
* Get refs where given attribute has a given value
* @param {String} attr
* @param {String} value
* @returns {Array<Object>}
*/
get(attr, value) {
attr = attr.toLowerCase();
var links = [];
for (var i = 0; i < this.refs.length; i++) {
if (this.refs[i][attr] === value) {
links.push(this.refs[i]);
}
}
return links;
}
set(link) {
this.refs.push(link);
return this;
}
has(attr, value) {
attr = attr.toLowerCase();
for (var i = 0; i < this.refs.length; i++) {
if (this.refs[i][attr] === value) {
return true;
}
}
return false;
}
parse(value, offset) {
offset = offset || 0;
value = offset ? value.slice(offset) : value;
// Trim & unfold folded lines
value = trim(value).replace(WS_FOLD_PATTERN, '');
var state = STATE.IDLE;
var length = value.length;
var offset = 0;
var ref = null;
while (offset < length) {
if (state === STATE.IDLE) {
if (hasWhitespace(value[offset])) {
offset++;
continue;
} else if (value[offset] === '<') {
if (ref != null) {
ref.rel != null
? this.refs.push(...Link.expandRelations(ref))
: this.refs.push(ref);
}
var end = value.indexOf('>', offset);
if (end === -1)
throw new Error(
'Expected end of URI delimiter at offset ' + offset
);
ref = { uri: value.slice(offset + 1, end) };
// this.refs.push( ref )
offset = end;
state = STATE.URI;
} else {
throw new Error(
'Unexpected character "' + value[offset] + '" at offset ' + offset
);
}
offset++;
} else if (state === STATE.URI) {
if (hasWhitespace(value[offset])) {
offset++;
continue;
} else if (value[offset] === ';') {
state = STATE.ATTR;
offset++;
} else if (value[offset] === ',') {
state = STATE.IDLE;
offset++;
} else {
throw new Error(
'Unexpected character "' + value[offset] + '" at offset ' + offset
);
}
} else if (state === STATE.ATTR) {
if (value[offset] === ';' || hasWhitespace(value[offset])) {
offset++;
continue;
}
var end = value.indexOf('=', offset);
if (end === -1)
throw new Error('Expected attribute delimiter at offset ' + offset);
var attr = trim(value.slice(offset, end)).toLowerCase();
var attrValue = '';
offset = end + 1;
offset = skipWhitespace(value, offset);
if (value[offset] === '"') {
offset++;
while (offset < length) {
if (value[offset] === '"') {
offset++;
break;
}
if (value[offset] === '\\') {
offset++;
}
attrValue += value[offset];
offset++;
}
} else {
var end = offset + 1;
while (!DELIMITER_PATTERN.test(value[end]) && end < length) {
end++;
}
attrValue = value.slice(offset, end);
offset = end;
}
if (ref[attr] && Link.isSingleOccurenceAttr(attr)) {
// Ignore multiples of attributes which may only appear once
} else if (attr[attr.length - 1] === '*') {
ref[attr] = Link.parseExtendedValue(attrValue);
} else {
attrValue = attr === 'type' ? attrValue.toLowerCase() : attrValue;
if (ref[attr] != null) {
if (Array.isArray(ref[attr])) {
ref[attr].push(attrValue);
} else {
ref[attr] = [ref[attr], attrValue];
}
} else {
ref[attr] = attrValue;
}
}
switch (value[offset]) {
case ',':
state = STATE.IDLE;
break;
case ';':
state = STATE.ATTR;
break;
}
offset++;
} else {
throw new Error('Unknown parser state "' + state + '"');
}
}
if (ref != null) {
ref.rel != null
? this.refs.push(...Link.expandRelations(ref))
: this.refs.push(ref);
}
ref = null;
return this;
}
toString() {
var refs = [];
var link = '';
var ref = null;
for (var i = 0; i < this.refs.length; i++) {
ref = this.refs[i];
link = Object.keys(this.refs[i]).reduce(function (link, attr) {
if (attr === 'uri') return link;
return link + '; ' + Link.formatAttribute(attr, ref[attr]);
}, '<' + ref.uri + '>');
refs.push(link);
}
return refs.join(', ');
}
}
/**
* Determines whether an encoding can be
* natively handled with a `Buffer`
* @param {String} value
* @returns {Boolean}
*/
Link.isCompatibleEncoding = function (value) {
return COMPATIBLE_ENCODING_PATTERN.test(value);
};
Link.parse = function (value, offset) {
return new Link().parse(value, offset);
};
Link.isSingleOccurenceAttr = function (attr) {
return (
attr === 'rel' ||
attr === 'type' ||
attr === 'media' ||
attr === 'title' ||
attr === 'title*'
);
};
Link.isTokenAttr = function (attr) {
return attr === 'rel' || attr === 'type' || attr === 'anchor';
};
Link.escapeQuotes = function (value) {
return value.replace(/"/g, '\\"');
};
Link.expandRelations = function (ref) {
var rels = ref.rel.split(' ');
return rels.map(function (rel) {
var value = Object.assign({}, ref);
value.rel = rel;
return value;
});
};
/**
* Parses an extended value and attempts to decode it
* @internal
* @param {String} value
* @return {Object}
*/
Link.parseExtendedValue = function (value) {
var parts = /([^']+)?(?:'([^']+)')?(.+)/.exec(value);
return {
language: parts[2].toLowerCase(),
encoding: Link.isCompatibleEncoding(parts[1])
? null
: parts[1].toLowerCase(),
value: Link.isCompatibleEncoding(parts[1])
? decodeURIComponent(parts[3])
: parts[3],
};
};
/**
* Format a given extended attribute and it's value
* @param {String} attr
* @param {Object} data
* @return {String}
*/
Link.formatExtendedAttribute = function (attr, data) {
var encoding = (data.encoding || 'utf-8').toUpperCase();
var language = data.language || 'en';
var encodedValue = '';
if (Buffer.isBuffer(data.value) && Link.isCompatibleEncoding(encoding)) {
encodedValue = data.value.toString(encoding);
} else if (Buffer.isBuffer(data.value)) {
encodedValue = data.value.toString('hex').replace(/[0-9a-f]{2}/gi, '%$1');
} else {
encodedValue = encodeURIComponent(data.value);
}
return attr + '=' + encoding + "'" + language + "'" + encodedValue;
};
/**
* Format a given attribute and it's value
* @param {String} attr
* @param {String|Object} value
* @return {String}
*/
Link.formatAttribute = function (attr, value) {
if (Array.isArray(value)) {
return value
.map((item) => {
return Link.formatAttribute(attr, item);
})
.join('; ');
}
if (attr[attr.length - 1] === '*' || typeof value !== 'string') {
return Link.formatExtendedAttribute(attr, value);
}
if (Link.isTokenAttr(attr)) {
value = needsQuotes(value)
? '"' + Link.escapeQuotes(value) + '"'
: Link.escapeQuotes(value);
} else if (needsQuotes(value)) {
value = encodeURIComponent(value);
// We don't need to escape <SP> <,> <;> within quotes
value = value
.replace(/%20/g, ' ')
.replace(/%2C/g, ',')
.replace(/%3B/g, ';');
value = '"' + value + '"';
}
return attr + '=' + value;
};
module.exports = Link;

18
src/types.ts Normal file
View File

@@ -0,0 +1,18 @@
import { Endpoints } from '@octokit/types';
export type SortedLanguageList = {
[language: string]: Star[];
};
export type PaginationLink = {
next: string;
last: string | undefined | null;
};
export type Stars = Endpoints['GET /user/starred']['response']['data'];
export type Star = Stars[number] | { language: string };
export type ApiGetStarResponse = {
links: PaginationLink;
data: Stars;
};