3 Commits

Author SHA1 Message Date
simonecorsi
7fb87741a2 chore(release): 1.1.0-next.1 [skip ci]
# [1.1.0-next.1](https://github.com/simonecorsi/mawesome/compare/v1.0.45...v1.1.0-next.1) (2022-04-13)

### Bug Fixes

* pre-tags ([602befc](602befcb54))

### Features

* **template:** adds templates ([791de9a](791de9ab50)), closes [#14](https://github.com/simonecorsi/mawesome/issues/14)
2022-04-13 13:54:00 +00:00
Simone Corsi
602befcb54 fix: pre-tags 2022-04-13 15:51:20 +02:00
Simone Corsi
791de9ab50 feat(template): adds templates
if an `TEMPLATE.ejs` file is found in the repo it will be used for rendering

closes #14
2022-04-13 15:34:08 +02:00
20 changed files with 30228 additions and 44174 deletions

21
.eslintrc Normal file
View File

@@ -0,0 +1,21 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"env": {
"es6": true,
"node": true
},
"extends": [
"prettier/@typescript-eslint",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"rules": {
"@typescript-eslint/no-var-requires": "off",
"prefer-rest-params": "off",
"@typescript-eslint/no-non-null-assertion": "off"
}
}

View File

@@ -1,17 +0,0 @@
module.exports = {
root: true,
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
env: {
node: true,
},
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended',
],
};

View File

@@ -1,39 +0,0 @@
name: Dependabot auto-merge
on: pull_request_target
jobs:
check:
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- uses: actions/setup-node@v4
with:
node-version: lts/*
- uses: actions/checkout@v4
with:
token: ${{ secrets.GH_TOKEN }}
persist-credentials: false
- name: Install dependencies
run: npm i
- name: Build
run: npm run build
dependabot:
needs: [check]
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v1.6.0
with:
github-token: '${{ secrets.GH_TOKEN }}'
- name: Enable auto-merge for Dependabot PRs
if: ${{ steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' }}
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GH_TOKEN }}

21
.github/workflows/merge.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Run tests
on: [pull_request]
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest]
node-version: [16.x]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm it

View File

@@ -3,16 +3,34 @@ name: Release
on: [workflow_dispatch] on: [workflow_dispatch]
jobs: jobs:
test:
strategy:
matrix:
os: [ubuntu-latest]
node-version: [16.x]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm it
release: release:
needs: [test]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 16
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
token: ${{ secrets.GH_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
persist-credentials: false
- name: Install dependencies - name: Install dependencies
run: npm i run: npm i
@@ -21,8 +39,8 @@ jobs:
run: npm run build run: npm run build
- name: Semantic release - name: Semantic release
uses: codfish/semantic-release-action@v3 uses: codfish/semantic-release-action@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_AUTHOR_NAME: '${{ github.actor }}' GIT_AUTHOR_NAME: '${{ github.actor }}'
GIT_COMMITTER_NAME: '${{ github.actor }}' GIT_COMMITTER_NAME: '${{ github.actor }}'

View File

@@ -10,20 +10,15 @@
[ [
"@semantic-release/git", "@semantic-release/git",
{ {
"assets": [ "assets": ["CHANGELOG.md", "package.json", "package-lock.json"]
"index.js",
"CHANGELOG.md",
"package.json",
"package-lock.json"
]
} }
], ],
"@semantic-release/github", "@semantic-release/github",
[ [
"@saithodev/semantic-release-backmerge", "@saithodev/semantic-release-backmerge",
{ {
"backmergeBranches": [{ "from": "main", "to": "next" }], "branches": [{ "from": "main", "to": "next" }],
"clearWorkspace": true "backmergeStrategy": "rebase"
} }
] ]
] ]

View File

@@ -1,64 +1,3 @@
# [2.2.0](https://github.com/simonecorsi/mawesome/compare/v2.1.0...v2.2.0) (2022-07-06)
### Bug Fixes
* compact by topic check ([49f6687](https://github.com/simonecorsi/mawesome/commit/49f66875e34ef186f183e370979ef751694d08b2))
* compact by topic check ([01cfb39](https://github.com/simonecorsi/mawesome/commit/01cfb39a41a2f60c709fd6dd6a46073149d96c20))
* docs ([4d7eef0](https://github.com/simonecorsi/mawesome/commit/4d7eef0991d896027ac464a40edfc564ced6bdd2))
### Features
* adds compacted by topics ([30fa351](https://github.com/simonecorsi/mawesome/commit/30fa351b37a04357303f59c79a90f92219f8072c))
* adds compacted by topics ([9dfa1f3](https://github.com/simonecorsi/mawesome/commit/9dfa1f3dae6c69033c682211c0e6bd9dc5a81eaf))
# [2.1.0](https://github.com/simonecorsi/mawesome/compare/v2.0.0...v2.1.0) (2022-05-11)
### Bug Fixes
* removes console.log ([7841262](https://github.com/simonecorsi/mawesome/commit/7841262e741f05debb7ffe6fed636a508a8f7c12))
### Features
* removes github-token need ([0dc2a51](https://github.com/simonecorsi/mawesome/commit/0dc2a51ddf3cf93414afd674ed3c34ec681f3e4b))
# [2.0.0](https://github.com/simonecorsi/mawesome/compare/v1.0.45...v2.0.0) (2022-05-11)
### Bug Fixes
* fixes default template path ([6cd9c30](https://github.com/simonecorsi/mawesome/commit/6cd9c30b20acb0789668b9fd4cdbace2cb52d3ce))
* fixes tsc build ([ec10b79](https://github.com/simonecorsi/mawesome/commit/ec10b79a91bc5894d35b80026d3e216420e0721a))
* normalize template file loadup ([445f562](https://github.com/simonecorsi/mawesome/commit/445f562fb50567d995f0d080d4267fc8d494731b))
* pre-tags ([d1d4edd](https://github.com/simonecorsi/mawesome/commit/d1d4edd104affc69984905c8408e859c25c58443))
* removes unused tests ([a141d23](https://github.com/simonecorsi/mawesome/commit/a141d23972c31b3dbd7e9841168219ad42fa7a18))
* reworking files ([19c4c8f](https://github.com/simonecorsi/mawesome/commit/19c4c8f761b244ddccbc445cc34078bf932559d2))
* updates gh-star-fetch ([3ec4b7c](https://github.com/simonecorsi/mawesome/commit/3ec4b7cd53c1fe885a51fb64279047a201d535dc))
### Features
* release major ([51a4359](https://github.com/simonecorsi/mawesome/commit/51a4359d983be4c842410f0c62104fca1b28252f))
* update to node16 ([e1f37af](https://github.com/simonecorsi/mawesome/commit/e1f37af978ebcb7f770949476ac7d6bc788a1fc2))
* updates deps ([7ade95d](https://github.com/simonecorsi/mawesome/commit/7ade95df8566a59145652165400cddfd1afa4bed))
* using gh-star-fetch ([346ba5d](https://github.com/simonecorsi/mawesome/commit/346ba5d4b7ba6a71bab99f2dbe3c2d010beb67d5))
* **template:** adds templates ([2c742b8](https://github.com/simonecorsi/mawesome/commit/2c742b820558fd715de987178303c460f5871c29)), closes [#14](https://github.com/simonecorsi/mawesome/issues/14)
### BREAKING CHANGES
* refactored code
# [1.1.0-next.2](https://github.com/simonecorsi/mawesome/compare/v1.1.0-next.1...v1.1.0-next.2) (2022-05-09)
### Features
* using gh-star-fetch ([a8b6577](https://github.com/simonecorsi/mawesome/commit/a8b657735b9879636cc039d79fddcdca33ccf38e))
# [1.1.0-next.1](https://github.com/simonecorsi/mawesome/compare/v1.0.45...v1.1.0-next.1) (2022-04-13) # [1.1.0-next.1](https://github.com/simonecorsi/mawesome/compare/v1.0.45...v1.1.0-next.1) (2022-04-13)

View File

@@ -8,12 +8,11 @@ You can see an example of the output at my own [simonecorsi/awesome](https://git
<!-- toc --> <!-- toc -->
- [Table of Contents](#table-of-contents)
- [Documentation](#documentation) - [Documentation](#documentation)
- [Requirements](#requirements) - [Requirements](#requirements)
- [Configuration](#configuration) - [Configuration](#configuration)
- [`api-token`](#api-token) - [`api-token`](#api-token)
- [`compact-by-topic`](#compact-by-topic)
- [`template-path`](#template-path)
- [Example workflow](#example-workflow) - [Example workflow](#example-workflow)
<!-- tocstop --> <!-- tocstop -->
@@ -22,44 +21,32 @@ You can see an example of the output at my own [simonecorsi/awesome](https://git
### Requirements ### Requirements
- An empty repository - An empty repository
- A personal github api key - A personal github api key
### Configuration ### Configuration
The service can be configured setting the appropriate environment variables or writing an `.env` file. The service can be configured setting the appropriate environment variables or writing an `.env` file.
| Variable | Description | Default | | Variable | Description | Default |
| ------------------ | ------------------------------------------------------------------- | -------------------------------- | | -------------- | ------------------------------------------- | -------------------------------- |
| `api-token` | Personal Token is used to avoid rate limit, [read more](#api-token) | `${{ secrets.API_TOKEN }}` | | `api-token` | Personal github api token. | `${{ secrets.API_TOKEN }}` |
| `compact-by-topic` | Render another list in `TOPICS.md` compacted by github topics | `'false'` | | `github-token` | Action Token | `${{ secrets.GITHUB_TOKEN }}` |
| `github-name` | Name used for the commit | Github Action | | `github-name` | Name used for the commit, default to action | Github Action |
| `github-email` | email used for commit | actions@users.noreply.github.com | | `github-email` | email used for commit, default to action | actions@users.noreply.github.com |
| `template-path` | Custom `README.md` template, [read more](#template-path) |
| `output-filename` | Output filename | `README.md` |
#### `api-token` #### `api-token`
The Personal API Access Token is mandatory to fetch stars from the API without incurring in Rate Limits. The Personal API Access Token is mandatory to fetch stars from the API without incurring in Rate Limits.
You'll have to generate a [personal api token](https://github.com/settings/tokens/new) and then add You'll have to generate a [personal api token](https://github.com/settings/tokens/new) and then add
#### `compact-by-topic`
If `compact-by-topic` is `'true'` it will generate another markdown file `TOPICS.md` whith all stars compacted by their github topics, be aware that this list will be bigger since data is duplicated.
#### `template-path`
If you don't like the output (default example [here](./TEMPLATE.ejs) ), you can provide your custom template that will be rendered using [EJS](https://ejs.co/) template engine.
**Path provided is relative to your current repository directory, if file is not found it will default.**
## Example workflow ## Example workflow
```yml ```yml
name: Update awesome list name: Update awesome list
on: on:
workflow_dispatch: workflow_dispatch:
schedule: schedule:
- cron: '0 0 * * *' - cron: '0 0 * * *'
@@ -76,4 +63,5 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
github-email: ${{ secrets.USER_EMAIL }} github-email: ${{ secrets.USER_EMAIL }}
github-name: ${{ github.repository_owner }} github-name: ${{ github.repository_owner }}
``` ```

View File

@@ -4,29 +4,16 @@ branding:
icon: align-justify icon: align-justify
color: yellow color: yellow
inputs: inputs:
compact-by-topic: github-token:
description: 'Generate another page with output compacted by github topics' description: 'Github token'
default: 'false' required: true
required: false
api-token: api-token:
description: 'Personal API Token' description: 'Personal API Token'
required: true required: true
github-name: github-name:
description: 'Name shown in the commit' description: 'Name shown in the commit'
default: 'GitHub Actions'
required: false
github-email: github-email:
description: 'Email shown in the commit' description: 'Email shown in the commit'
default: 'actions@users.noreply.github.com'
required: false
template-path:
required: false
description: 'EJS template path relative to project root directory'
default: 'TEMPLATE.ejs'
output-filename:
description: 'The output file name, default to README.md'
required: false
default: 'README.md'
runs: runs:
using: 'node16' using: 'node12'
main: 'index.js' main: 'index.js'

7
ava.config.js Normal file
View File

@@ -0,0 +1,7 @@
/* eslint-disable node/no-unsupported-features/es-syntax */
export default {
files: ['!templates/**/*'],
extensions: ['ts'],
require: ['ts-node/register/transpile-only'],
};

55365
index.js

File diff suppressed because one or more lines are too long

17968
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,18 @@
{ {
"name": "mawesome", "name": "mawesome",
"version": "2.2.0", "version": "1.1.0-next.1",
"description": "Generate awesome list from user starred repositories", "description": "Generate awesome list from user starred repositories",
"main": "index.js", "main": "index.js",
"author": "Simone Corsi<simonecorsi.dev@gmail.com>", "author": "Simone Corsi<simonecorsi.dev@gmail.com>",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"test": "nyc --reporter=lcov --reporter=text-summary ava -s -v",
"test:watch": "ava -w",
"style:lint": "eslint src --ext .ts", "style:lint": "eslint src --ext .ts",
"style:prettier": "prettier \"src/**/*.ts\" --list-different --write", "style:prettier": "prettier \"src/**/*.ts\" --list-different --write",
"build": "./node_modules/.bin/ncc build src/index.ts -o ./", "build": "./node_modules/.bin/ncc build src/index.ts -o ./",
"dev": "ts-node-dev src/index.ts", "dev": "ts-node-dev src/index.ts",
"prerelease": "npm run build",
"prepare": "node prepare.js || echo 'Skipping prepare'" "prepare": "node prepare.js || echo 'Skipping prepare'"
}, },
"keywords": [ "keywords": [
@@ -22,44 +25,44 @@
"javascript" "javascript"
], ],
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^18.6.1", "@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^18.6.2", "@commitlint/config-conventional": "^16.2.1",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^18.12.0",
"@octokit/types": "^12.6.0", "@octokit/types": "^6.2.1",
"@saithodev/semantic-release-backmerge": "^4.0.1", "@saithodev/semantic-release-backmerge": "^2.1.2",
"@semantic-release/changelog": "^6.0.3", "@semantic-release/changelog": "^6.0.1",
"@semantic-release/commit-analyzer": "^11.1.0", "@semantic-release/commit-analyzer": "^9.0.2",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^9.2.6", "@semantic-release/github": "^8.0.4",
"@semantic-release/release-notes-generator": "^12.1.0", "@semantic-release/release-notes-generator": "^10.0.3",
"@types/ejs": "^3.1.5", "@types/ejs": "^3.0.5",
"@types/got": "^9.6.12", "@types/got": "^9.6.12",
"@types/node": "^20.11.28", "@types/node": "^14.14.5",
"@types/sinon": "^17.0.3", "@types/sinon": "^9.0.10",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^4.6.0",
"@vercel/ncc": "^0.38.1", "@vercel/ncc": "^0.33.3",
"eslint": "^8.57.0", "ava": "^3.8.2",
"eslint-config-prettier": "^9.1.0", "eslint": "^7.17.0",
"eslint-config-prettier": "^7.1.0",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^3.3.1",
"husky": "^8.0.3", "husky": "^7.0.4",
"lint-staged": "^15.2.2", "lint-staged": "^12.3.7",
"markdown-toc": "^1.2.0", "markdown-toc": "^1.2.0",
"nyc": "^15.0.1", "nyc": "^15.0.1",
"prettier": "^3.2.5", "prettier": "^2.0.5",
"sinon": "^17.0.1", "sinon": "^9.2.3",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^1.1.1",
"typescript": "^5.4.2" "typescript": "^4.1.3"
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.10.1", "@actions/core": "^1.2.6",
"@actions/exec": "^1.1.1", "@actions/exec": "^1.1.1",
"ejs": "^3.1.9", "ejs": "^3.1.6",
"gh-star-fetch": "^1.5.0", "got": "^11.8.1",
"got": "^11.8.5", "remark": "^13.0.0",
"remark": "^15.0.1", "remark-toc": "^7.0.0"
"remark-toc": "^9.0.0"
}, },
"volta": { "volta": {
"node": "16.14.2", "node": "16.14.2",

11
src/api.ts Normal file
View File

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

View File

@@ -2,20 +2,13 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as exec from '@actions/exec'; import * as exec from '@actions/exec';
import fs from 'fs/promises';
const { GITHUB_REPOSITORY, GITHUB_REF } = process.env; const { GITHUB_REPOSITORY, GITHUB_REF } = process.env;
const branch = GITHUB_REF?.replace('refs/heads/', ''); const branch = GITHUB_REF?.replace('refs/heads/', '');
type File = {
filename: string;
data: string;
};
class Git { class Git {
constructor() { constructor() {
const githubToken = core.getInput('api-token', { required: true }); const githubToken = core.getInput('github-token', { required: true });
core.setSecret(githubToken); core.setSecret(githubToken);
const githubName = core.getInput('github-name') || 'GitHub Actions'; const githubName = core.getInput('github-name') || 'GitHub Actions';
@@ -41,26 +34,28 @@ class Git {
return isShallow.trim().replace('\n', '') === 'true'; return isShallow.trim().replace('\n', '') === 'true';
}; };
async exec(command: string): Promise<string> { exec = (command: string): Promise<string> => {
let execOutput = ''; return new Promise(async (resolve, reject) => {
let execOutput = '';
const options = { const options = {
listeners: { listeners: {
stdout: (data: Buffer) => { stdout: (data: Buffer) => {
execOutput += data.toString(); execOutput += data.toString();
},
}, },
}, };
};
const exitCode = await exec.exec(`git ${command}`, undefined, options); const exitCode = await exec.exec(`git ${command}`, undefined, options);
if (exitCode === 0) { if (exitCode === 0) {
return execOutput; return resolve(execOutput);
} else { } else {
core.error(`Command "git ${command}" exited with code ${exitCode}.`); core.error(`Command "git ${command}" exited with code ${exitCode}.`);
throw new Error(`Command "git ${command}" exited with code ${exitCode}.`); return reject(`Command "git ${command}" exited with code ${exitCode}.`);
} }
} });
};
config = (prop: string, value: string) => config = (prop: string, value: string) =>
this.exec(`config ${prop} "${value}"`); this.exec(`config ${prop} "${value}"`);
@@ -96,20 +91,6 @@ class Git {
updateOrigin = (repo: string) => this.exec(`remote set-url origin ${repo}`); updateOrigin = (repo: string) => this.exec(`remote set-url origin ${repo}`);
createTag = (tag: string) => this.exec(`tag -a ${tag} -m "${tag}"`); createTag = (tag: string) => this.exec(`tag -a ${tag} -m "${tag}"`);
async pushNewFiles(files: File[] = []): Promise<unknown> {
if (!files.length) return;
await this.pull();
await Promise.all(
files.map(({ filename, data }) => fs.writeFile(filename, data))
);
await this.add(files.map(({ filename }) => filename));
await this.commit(`chore(updates): updated entries in files`);
await this.push();
}
} }
export default new Git(); export default new Git();

View File

@@ -1,14 +1,28 @@
import fs from 'fs';
import ejs from 'ejs'; import ejs from 'ejs';
import * as core from '@actions/core'; import * as core from '@actions/core';
import { remark } from 'remark'; import remark from 'remark';
import toc from 'remark-toc'; import toc from 'remark-toc';
import MD_TEMPLATE from './template';
import GithubApi from './api';
import link from './link';
import git from './git';
import type { PaginationLink, ApiGetStarResponse, Stars, Star } from './types';
export const REPO_USERNAME = process.env.GITHUB_REPOSITORY?.split('/')[0]; export const REPO_USERNAME = process.env.GITHUB_REPOSITORY?.split('/')[0];
export const API_STARRED_URL = `${process.env.GITHUB_API_URL}/users/${REPO_USERNAME}/starred`; export const API_STARRED_URL = `${process.env.GITHUB_API_URL}/users/${REPO_USERNAME}/starred`;
const fsp = fs.promises;
export function wait(time = 200): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, time));
}
export async function renderer( export async function renderer(
data: { [key: string]: unknown }, data: { [key: string]: any },
templateString: string templateString = MD_TEMPLATE
): Promise<string> { ): Promise<string> {
try { try {
return ejs.render(templateString, data); return ejs.render(templateString, data);
@@ -18,6 +32,74 @@ export async function renderer(
} }
} }
export function getNextPage(links: PaginationLink[]): string | null {
const next = links.find((l) => l.rel === 'next');
const last = links.find((l) => l.rel === 'last');
if (!next || !last) return null;
const matchNext = next.uri.match(/page=([0-9]*)/);
const matchLast = last.uri.match(/page=([0-9]*)/);
if (!matchNext || !matchLast) return null;
if (matchNext[1] === matchLast[1]) return null;
return matchNext[1];
}
async function* paginateStars(url: string): AsyncGenerator<Stars> {
let nextPage: string | null = '1';
while (nextPage) {
try {
const { headers, body } = await GithubApi.get(url, {
searchParams: {
page: nextPage,
},
});
yield (body as unknown) as Stars;
nextPage = getNextPage(link.parse(headers.link).refs);
await wait(1000); // avoid limits
} catch (e) {
console.error(e);
break;
}
}
}
export async function apiGetStar(
url: string = API_STARRED_URL
): Promise<ApiGetStarResponse> {
const data: Partial<Star>[] = [];
for await (const stars of paginateStars(url)) {
for (const star of stars) {
data.push({
id: star.id,
node_id: star.node_id,
name: star.name,
full_name: star.full_name,
owner: {
login: star?.owner?.login,
id: star?.owner?.id,
avatar_url: star?.owner?.avatar_url,
url: star?.owner?.url,
html_url: star?.owner?.html_url,
},
html_url: star.html_url,
description: star.description,
url: star.url,
languages_url: star.languages_url,
created_at: star.created_at,
updated_at: star.updated_at,
git_url: star.git_url,
ssh_url: star.ssh_url,
clone_url: star.clone_url,
homepage: star.homepage,
stargazers_count: star.stargazers_count,
watchers_count: star.watchers_count,
language: star.language,
topics: star.topics,
} as Partial<Star>);
}
}
return (data as unknown) as Stars;
}
export function generateMd(data: string): Promise<string> { export function generateMd(data: string): Promise<string> {
return new Promise((resolve) => { return new Promise((resolve) => {
remark() remark()
@@ -26,11 +108,31 @@ export function generateMd(data: string): Promise<string> {
if (error) { if (error) {
core.error('#generateMd'); core.error('#generateMd');
core.error(error); core.error(error);
resolve(''); return resolve('');
} }
resolve(String(file)); return resolve(String(file));
}); });
}); });
} }
export const MARKDOWN_FILENAME: string = core.getInput('output-filename'); export const MARKDOWN_FILENAME: string =
core.getInput('output-filename') || 'README.md';
type File = {
filename: string;
data: string;
};
export async function pushNewFiles(files: File[] = []): Promise<any> {
if (!files.length) return;
await git.pull();
await Promise.all(
files.map(({ filename, data }) => fsp.writeFile(filename, data))
);
await git.add(files.map(({ filename }) => filename));
await git.commit(`chore(updates): updated entries in files`);
await git.push();
}

View File

@@ -1,85 +1,68 @@
import path from 'path';
import * as core from '@actions/core'; import * as core from '@actions/core';
import { readFile } from 'fs/promises'; import { readdir, readFile } from 'fs/promises';
import ghStarFetch, {
Options,
compactByLanguage,
compactByTopic,
} from 'gh-star-fetch';
import { import {
renderer, renderer,
REPO_USERNAME, REPO_USERNAME,
generateMd, generateMd,
pushNewFiles,
MARKDOWN_FILENAME, MARKDOWN_FILENAME,
apiGetStar,
} from './helpers'; } from './helpers';
import git from './git';
export async function main() { import type { SortedLanguageList, Stars, Star } from './types';
// set default template
let template = await readFile( export async function main(): Promise<any> {
path.resolve(__dirname, './TEMPLATE.ejs'), const results: Stars = await apiGetStar();
'utf8'
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;
},
{}
); );
// get template if found in the repo // get template if found in the repo
const customTemplatePath = core.getInput('template-path'); let template;
core.info(`check if customTemplatePath: ${customTemplatePath} exists`);
try { try {
template = await readFile(customTemplatePath, 'utf8'); const dir = await readdir('./');
core.info(dir.join('\n'));
template = await readFile('TEMPLATE.ejs', 'utf8');
core.info(template);
} catch { } catch {
core.info("Couldn't find template file, using default"); core.warning("Couldn't find template file, using default");
} }
const opts: Partial<Options> = { const rendered = await renderer(
accessToken: core.getInput('api-token', { required: true }),
};
const results = await ghStarFetch(opts);
const files = [];
const compactedByLanguage = compactByLanguage(results);
const byLanguage = await renderer(
{ {
username: REPO_USERNAME, username: REPO_USERNAME,
stars: Object.entries(compactedByLanguage), stars: Object.entries(sortedByLanguages),
updatedAt: Date.now(), updatedAt: Date.now(),
}, },
template template
); );
files.push( const markdown: string = await generateMd(rendered);
await pushNewFiles([
{ {
filename: MARKDOWN_FILENAME, filename: MARKDOWN_FILENAME,
data: await generateMd(byLanguage), data: markdown,
}, },
{ {
filename: 'data.json', filename: 'data.json',
data: JSON.stringify(compactedByLanguage, null, 2), data: JSON.stringify(sortedByLanguages, null, 2),
} },
); ]);
if (core.getInput('compact-by-topic') === 'true') {
const compactedByTopic = compactByTopic(results);
const byTopic = await renderer(
{
username: REPO_USERNAME,
stars: Object.entries(compactedByTopic),
updatedAt: Date.now(),
},
template
);
files.push({
filename: 'TOPICS.md',
data: await generateMd(byTopic),
});
}
await git.pushNewFiles(files);
} }
export async function run(): Promise<void> { export async function run(): Promise<any> {
try { try {
await main(); await main();
} catch (error) { } catch (error) {
@@ -87,11 +70,10 @@ export async function run(): Promise<void> {
} }
} }
const catchAll = (info: string) => { const catchAll = (info: any) => {
core.setFailed(`#catchAll: ${info}`); core.setFailed(`#catchAll: ${info}`);
core.error(info);
}; };
process.on('unhandledRejection', catchAll); process.on('unhandledRejection', catchAll);
process.on('uncaughtException', catchAll); process.on('uncaughtException', catchAll);
run().catch(core.error); run();

384
src/link.js Normal file
View File

@@ -0,0 +1,384 @@
/**
* Copyright (c) 2016 Jonas Hermsmeier
* https://github.com/jhermsmeier/node-http-link-header
*/
/* istanbul ignore file */
'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;

View File

@@ -1,4 +1,4 @@
# <%= username %> Awesome List [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/sindresorhus/awesome) export default `# <%= username %> Awesome List [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/sindresorhus/awesome)
> :star: generated with [simonecorsi/mawesome](https://github.com/simonecorsi/mawesome) > :star: generated with [simonecorsi/mawesome](https://github.com/simonecorsi/mawesome)
@@ -11,3 +11,4 @@
<% } %> <% } %>
<% } %> <% } %>
`;

79
test/helpers.spec.ts Normal file
View File

@@ -0,0 +1,79 @@
import test from 'ava';
import * as sinon from 'sinon';
import fs from 'fs';
import * as core from '@actions/core';
sinon.replace(core, 'getInput', sinon.fake());
import GithubApi from '../src/api';
const GithubApiFake = sinon.fake((rul) => ({
body: [],
headers: {
link:
'<https://api.github.com/user/5617452/starred?page=2>; rel="next", <https://api.github.com/user/5617452/starred?page=2>; rel="last"',
},
}));
sinon.replace(GithubApi, 'get', GithubApiFake);
import Git from '../src/git';
const pull = sinon.fake();
sinon.replace(Git, 'pull', pull);
const add = sinon.fake();
sinon.replace(Git, 'add', add);
const commit = sinon.fake();
sinon.replace(Git, 'commit', commit);
const push = sinon.fake();
sinon.replace(Git, 'push', push);
sinon.replace(Git, 'config', sinon.fake());
sinon.replace(Git, 'updateOrigin', sinon.fake());
const fsp = fs.promises;
const writeFile = sinon.fake();
sinon.replace(fsp, 'writeFile', writeFile);
import {
wait,
renderer,
apiGetStar,
generateMd,
pushNewFiles,
} from '../src/helpers';
test('wait should wait', async (t) => {
await wait(200);
t.pass();
});
test('renderer should render', async (t) => {
const output = await renderer({ variable: 123 }, 'Test: <%= variable %>');
t.is(output, 'Test: 123');
});
test('apiGetStar', async (t) => {
let stars = await apiGetStar('url');
t.true(GithubApiFake.called);
t.true(Array.isArray(stars));
});
test('generateMd should create TOC', async (t) => {
const tpl = `# title
## Table of Contents
## Javascript
`;
const result = await generateMd(tpl);
t.is(
result,
`# title\n\n## Table of Contents\n\n* [Javascript](#javascript)\n\n## Javascript\n`
);
});
test('should push', async (t) => {
await pushNewFiles([{ filename: 'README.md', data: '# title' }]);
t.true(writeFile.called);
t.true(pull.called);
t.true(add.called);
t.true(commit.called);
t.true(push.called);
});