├── .eslintignore ├── .prettierignore ├── src ├── @types │ └── index.d.ts ├── getIndices.ts ├── main.ts ├── extractBody.ts ├── getContents.ts ├── createTableContents.ts └── modifyReadme.ts ├── .prettierrc.json ├── jest.config.js ├── tsconfig.json ├── action.yml ├── .eslintrc.json ├── .github └── workflows │ └── rewriteReadme.yml ├── __tests__ └── main.test.ts ├── .gitignore ├── package.json ├── LICENSE └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /src/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'tablemark'; 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /src/getIndices.ts: -------------------------------------------------------------------------------- 1 | const getIndices = async (text: string, pattern: string) => { 2 | const firstIndex = text.indexOf(pattern); 3 | const lastIndex = text.lastIndexOf(pattern); 4 | 5 | return [firstIndex + pattern.length, lastIndex]; 6 | }; 7 | 8 | export default getIndices; 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | }; 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "module": "commonjs", 6 | "outDir": "./lib", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "esModuleInterop": true 11 | }, 12 | "exclude": ["node_modules", "**/*.test.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import modifyReadme from './modifyReadme'; 3 | import { writeFile } from 'fs'; 4 | 5 | async function run(): Promise { 6 | try { 7 | console.log('Initializing.'); 8 | 9 | const newReadme = await modifyReadme(); 10 | 11 | writeFile('./README.md', newReadme, () => 12 | console.log('New file has been written.') 13 | ); 14 | } catch (error) { 15 | core.setFailed(error.message); 16 | } 17 | } 18 | 19 | run(); 20 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "Auto Issue List in README" 2 | description: "Create issue list in README.md" 3 | author: "seed-of-apricot " 4 | branding: 5 | icon: check-circle 6 | color: orange 7 | inputs: 8 | GITHUB_TOKEN: 9 | description: "token" 10 | required: true 11 | pattern: 12 | description: "pattern to detect the place to locate the table" 13 | required: false 14 | default: "" 15 | labels: 16 | description: "comma-separated labels to filter the issues" 17 | required: false 18 | state: 19 | description: "state of the issues to filter them (open or closed)" 20 | default: "all" 21 | required: false 22 | runs: 23 | using: "node12" 24 | main: "dist/index.js" 25 | -------------------------------------------------------------------------------- /src/extractBody.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import getIndices from './getIndices'; 3 | 4 | const extractBody = async (text: string) => { 5 | let extracted; 6 | const pattern = core.getInput('pattern'); 7 | const [firstIndex, lastIndex] = await getIndices(text, pattern); 8 | 9 | if (firstIndex === -1 || lastIndex === -1) { 10 | extracted = text; 11 | } else { 12 | extracted = text.substring(firstIndex + 1, lastIndex - 1); 13 | } 14 | 15 | const strArray = extracted.split('\r\n'); 16 | 17 | const newText = strArray 18 | .slice(0, 3) 19 | .map(item => item.replace('\n', '').replace('\r', '')) 20 | .join('
'); 21 | 22 | return strArray.length > 3 ? newText + '
...' : newText; 23 | }; 24 | 25 | export default extractBody; 26 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["jest", "@typescript-eslint"], 3 | "extends": [ 4 | "plugin:github/es6", 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "prettier/@typescript-eslint", 8 | "plugin:prettier/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": 2019, 13 | "sourceType": "module", 14 | "project": "./tsconfig.json" 15 | }, 16 | "rules": { 17 | "eslint-comments/no-use": "off", 18 | "import/no-namespace": "off", 19 | "no-unused-vars": "off", 20 | "@typescript-eslint/explicit-function-return-type": "off", 21 | "@typescript-eslint/prefer-interface": "off", 22 | "no-console": "off" 23 | }, 24 | "env": { 25 | "node": true, 26 | "es6": true, 27 | "jest/globals": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/rewriteReadme.yml: -------------------------------------------------------------------------------- 1 | name: "rewriteReadme" 2 | on: 3 | issues: 4 | types: [labeled, unlabeled] 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: checkout 11 | uses: actions/checkout@v2 12 | - name: rewriteReadme 13 | uses: seed-of-apricot/issue-list-readme@master 14 | with: 15 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 16 | pattern: "" 17 | labels: "good first issue" 18 | state: "open" 19 | - name: add-and-commit 20 | uses: EndBug/add-and-commit@v4 21 | with: 22 | message: README.md has been re-written 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | - name: push 26 | uses: ad-m/github-push-action@master 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /src/getContents.ts: -------------------------------------------------------------------------------- 1 | import * as github from '@actions/github'; 2 | import * as core from '@actions/core'; 3 | import { readFileSync } from 'fs'; 4 | 5 | const getContents = async () => { 6 | try { 7 | const token = core.getInput('GITHUB_TOKEN'); 8 | const labels = core.getInput('labels'); 9 | const state = core.getInput('state') as 'open' | 'closed' | 'all'; 10 | const octokit = new github.GitHub(token); 11 | 12 | console.log('GitHub client has been initialized.'); 13 | 14 | const repository = github.context.repo; 15 | 16 | const list = await octokit.issues.listForRepo({ 17 | ...repository, 18 | state, 19 | labels 20 | }); 21 | const readme = readFileSync('./README.md'); 22 | 23 | return { 24 | issues: list.data, 25 | readme: readme.toString() 26 | }; 27 | } catch (error) { 28 | core.setFailed(error.message); 29 | throw error.message; 30 | } 31 | }; 32 | 33 | export default getContents; 34 | -------------------------------------------------------------------------------- /__tests__/main.test.ts: -------------------------------------------------------------------------------- 1 | import { wait } from '../src/wait'; 2 | import * as process from 'process'; 3 | import * as cp from 'child_process'; 4 | import * as path from 'path'; 5 | 6 | test('throws invalid number', async () => { 7 | const input = parseInt('foo', 10); 8 | await expect(wait(input)).rejects.toThrow('milliseconds not a number'); 9 | }); 10 | 11 | test('wait 500 ms', async () => { 12 | const start = new Date(); 13 | await wait(500); 14 | const end = new Date(); 15 | const delta = Math.abs(end.getTime() - start.getTime()); 16 | expect(delta).toBeGreaterThan(450); 17 | }); 18 | 19 | // shows how the runner will run a javascript action with env / stdout protocol 20 | test('test runs', () => { 21 | process.env['INPUT_MILLISECONDS'] = '500'; 22 | const ip = path.join(__dirname, '..', 'lib', 'main.js'); 23 | const options: cp.ExecSyncOptions = { 24 | env: process.env 25 | }; 26 | console.log(cp.execSync(`node ${ip}`, options).toString()); 27 | }); 28 | -------------------------------------------------------------------------------- /src/createTableContents.ts: -------------------------------------------------------------------------------- 1 | import tablemark from 'tablemark'; 2 | import * as core from '@actions/core'; 3 | import extractBody from './extractBody'; 4 | 5 | const createTableContents = async (issues: any[]) => { 6 | try { 7 | const array = issues.map(async (item: any) => ({ 8 | title: `${item.title}`, 9 | status: item.state === 'open' ? ':eight_spoked_asterisk:' : ':no_entry:', 10 | assignee: item.assignees.map( 11 | (assignee: any) => 12 | `` 13 | ), 14 | body: await extractBody(item.body) 15 | })); 16 | 17 | const markDownText: string = tablemark(await Promise.all(array), { 18 | columns: [ 19 | { align: 'left' }, 20 | { align: 'center' }, 21 | { align: 'center' }, 22 | { align: 'left' } 23 | ] 24 | }); 25 | 26 | return markDownText; 27 | } catch (error) { 28 | core.setFailed(error.message); 29 | throw error.message; 30 | } 31 | }; 32 | export default createTableContents; 33 | -------------------------------------------------------------------------------- /src/modifyReadme.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import createTableContents from './createTableContents'; 3 | import getContents from './getContents'; 4 | import getIndices from './getIndices'; 5 | 6 | const modifyReadme = async () => { 7 | try { 8 | const pattern = core.getInput('pattern'); 9 | const contents = await getContents(); 10 | 11 | console.log('Contents has been retrieved.'); 12 | 13 | const [firstIndex, lastIndex] = await getIndices(contents.readme, pattern); 14 | 15 | if (firstIndex === -1 || lastIndex === -1) { 16 | core.setFailed('notValidIndexException'); 17 | throw 'notValidIndexException'; 18 | } 19 | 20 | const beforeTable = contents.readme.substring(0, firstIndex); 21 | const afterTable = contents.readme.substring(lastIndex); 22 | 23 | console.log('Table wrapper has been identified.'); 24 | 25 | const table = await createTableContents(contents.issues); 26 | 27 | console.log('Table has been created.'); 28 | 29 | return beforeTable + '\n\n' + table + '\n' + afterTable; 30 | } catch (error) { 31 | core.setFailed(error.message); 32 | throw error.message; 33 | } 34 | }; 35 | 36 | export default modifyReadme; 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | 93 | # OS metadata 94 | .DS_Store 95 | Thumbs.db 96 | 97 | # Ignore built ts files 98 | __tests__/runner/* 99 | lib/**/* -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-action", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "TypeScript template action", 6 | "main": "lib/main.js", 7 | "scripts": { 8 | "build": "tsc", 9 | "format": "prettier --write **/*.ts", 10 | "format-check": "prettier --check **/*.ts", 11 | "lint": "eslint src/**/*.ts", 12 | "pack": "ncc build", 13 | "dev": "ncc build -w", 14 | "test": "jest", 15 | "all": "npm run build && npm run format && npm run lint && npm run pack && npm test" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/actions/typescript-action.git" 20 | }, 21 | "keywords": [ 22 | "actions", 23 | "node", 24 | "setup" 25 | ], 26 | "author": "YourNameOrOrganization", 27 | "license": "MIT", 28 | "dependencies": { 29 | "@actions/core": "^1.2.3", 30 | "@actions/github": "^2.1.1" 31 | }, 32 | "devDependencies": { 33 | "@octokit/action": "^2.0.3", 34 | "@types/jest": "^24.0.23", 35 | "@types/node": "^12.7.12", 36 | "@typescript-eslint/eslint-plugin": "^2.25.0", 37 | "@typescript-eslint/parser": "^2.8.0", 38 | "@zeit/ncc": "^0.20.5", 39 | "eslint": "^5.16.0", 40 | "eslint-config-prettier": "^6.10.1", 41 | "eslint-plugin-github": "^2.0.0", 42 | "eslint-plugin-jest": "^22.21.0", 43 | "eslint-plugin-prettier": "^3.1.2", 44 | "graphql-tag": "^2.10.3", 45 | "husky": "^4.2.3", 46 | "jest": "^24.9.0", 47 | "jest-circus": "^24.9.0", 48 | "js-yaml": "^3.13.1", 49 | "lint-staged": "^10.0.10", 50 | "prettier": "^1.19.1", 51 | "tablemark": "^2.0.0", 52 | "ts-jest": "^24.2.0", 53 | "typescript": "^3.6.4" 54 | }, 55 | "husky": { 56 | "hooks": { 57 | "pre-commit": [ 58 | "lint-staged" 59 | ] 60 | } 61 | }, 62 | "lint-staged": { 63 | "./**/*.{js,ts,jsx,tsx}": [ 64 | "eslint" 65 | ], 66 | "./**/*.{js,ts,jsx,tsx,json,md}": [ 67 | "prettier --write", 68 | "git add" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 seed.of.apricot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | --- 24 | 25 | The MIT License (MIT) 26 | 27 | Copyright (c) 2018 GitHub, Inc. and contributors 28 | 29 | Permission is hereby granted, free of charge, to any person obtaining a copy 30 | of this software and associated documentation files (the "Software"), to deal 31 | in the Software without restriction, including without limitation the rights 32 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 33 | copies of the Software, and to permit persons to whom the Software is 34 | furnished to do so, subject to the following conditions: 35 | 36 | The above copyright notice and this permission notice shall be included in 37 | all copies or substantial portions of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 40 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 41 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 42 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 43 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 44 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 45 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![rewriteReadme](https://github.com/seed-of-apricot/issue-list-readme/workflows/rewriteReadme/badge.svg) 2 | 3 | # Auto Issue List in README 4 | 5 | Display issues on the landing page :eyes: 6 | 7 | ## Demo 8 | 9 | The issue list below... 10 | 11 | 12 | 13 | | Title | Status | Assignee | Body | 14 | | :------------------------------------------------------------------------------------ | :---------------------: | :-----------------------------------------------------------------------------------------------------------------------------: | :------------------------------------ | 15 | | more test | :eight_spoked_asterisk: | | yay! | 16 | | test | :eight_spoked_asterisk: | | test
hoge
testtest
... | 17 | 18 | 19 | 20 | is compiled through this action. 21 | 22 | ```yml 23 | # setup 24 | steps: 25 | - name: checkout 26 | uses: actions/checkout@v2 27 | - name: rewriteReadme 28 | uses: seed-of-apricot/issue-list-readme@v1.1.0 29 | with: 30 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 31 | pattern: '' # suffixing "Demo" for this readme 32 | labels: 'good first issue' 33 | state: 'open' 34 | # you need to add, commit and push here 35 | # otherwise the changes will not be pushed back into master 36 | ``` 37 | 38 | ## Usage 39 | 40 | 1. Place two identifiers in your README.md, which the action detects as the location to inject the issue list to. 41 | 42 | ```md 43 | README.md 44 | 45 | // 46 | // your contents 47 | // 48 | 49 | // place two identifiers 50 | 51 | 52 | 53 | 54 | 55 | // 56 | // your another contents 57 | // 58 | ``` 59 | 60 | 2. Add some issues. You can optionally use the same identifier to extract a part the body to display in the list. 61 | 62 | ```md 63 | Some issue 64 | 65 | // place two identifiers (optional) 66 | 67 | 68 | 69 | 70 | 71 | // 72 | // your comment 73 | // 74 | ``` 75 | 76 | 3. Then, write the following section into your action. 77 | 78 | ```yml 79 | uses: seed-of-apricot/issue-list-readme@v1.1.0 80 | with: 81 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 82 | ``` 83 | 84 | The action will fetch the issues in the repository and re-write the README.md :thumbsup: 85 | The body will show up to three first lines of the body (or the extracted part of it). 86 | 87 | It is recommended to run this action on issue open/close or by a cron job (e.g., `cron: "0 0 * * *"`). 88 | 89 | ## Options 90 | 91 | Available variables are: 92 | 93 | | variable | required | default | note | 94 | | ------------ | -------- | ----------------------- | ---------------------------------------------------------------------------------- | 95 | | GITHUB_TOKEN | true | - | you can use \${{ secrets.GITHUB_TOKEN }} | 96 | | pattern | false | "\