├── .github └── FUNDING.yml ├── .gitignore ├── .glitch-assets ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── app.yml ├── index.js ├── package.json ├── screenshots ├── example-pr.png ├── failing-pr.png ├── pending-pr.png ├── success-pr.png ├── tasks-completed.png └── tasks-remaining.png ├── src └── check-outstanding-tasks.js └── tests └── index.test.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: stilliard 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ["https://www.buymeacoffee.com/stilliard"] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | *.pem 4 | .env 5 | .env-* 6 | package-lock.json 7 | .config/ 8 | shrinkwrap.yaml 9 | -------------------------------------------------------------------------------- /.glitch-assets: -------------------------------------------------------------------------------- 1 | {"name":"drag-in-files.svg","date":"2016-10-22T16:17:49.954Z","url":"https://cdn.hyperdev.com/drag-in-files.svg","type":"image/svg","size":7646,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/drag-in-files.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(102, 153, 205)","uuid":"adSBq97hhhpFNUna"} 2 | {"name":"click-me.svg","date":"2016-10-23T16:17:49.954Z","url":"https://cdn.hyperdev.com/click-me.svg","type":"image/svg","size":7116,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/click-me.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(243, 185, 186)","uuid":"adSBq97hhhpFNUnb"} 3 | {"name":"paste-me.svg","date":"2016-10-24T16:17:49.954Z","url":"https://cdn.hyperdev.com/paste-me.svg","type":"image/svg","size":7242,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/paste-me.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(42, 179, 185)","uuid":"adSBq97hhhpFNUnc"} 4 | {"name":"deploy.md","date":"2017-10-26T18:17:58.460Z","url":"https://cdn.glitch.com/ba4e6442-32a5-45f7-b695-ac607062c845%2Fdeploy.md","type":"text/markdown","size":368,"thumbnail":"https://cdn.glitch.com/ba4e6442-32a5-45f7-b695-ac607062c845%2Fthumbnails%2Fdeploy.md","thumbnailWidth":210,"thumbnailHeight":210,"dominantColor":"rgba(191, 175, 247, 0.60)","uuid":"h1XzkIuvA61wFfr5"} 5 | {"uuid":"h1XzkIuvA61wFfr5","deleted":true} 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "8" 5 | notifications: 6 | disabled: true 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at andrew@stapps.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Andrew Stilliard 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | GitHub - Task list completed PR check 5 | ========================= 6 | 7 | Install from the Marketplace: https://github.com/marketplace/task-list-completed 8 | 9 | Check a pull request body for task lists / checkboxes / tickboxes & make sure they are all completed. 10 | The check will not pass until all task lists have been checked. 11 | 12 | **Mark it as a required check to preventing merging the PR until all tasks in a PR have been ticked off.** 13 | 14 | You can use this to check manual tests or requirements have been ticked off before the pull request can be merged. 15 | 16 | E.g. Say you add some tasks like so 17 | ``` 18 | - [x] Check the size looks good on the front end 19 | - [ ] Check the image is centered 20 | ``` 21 | 22 | & they display like this to be ticked off as tests: 23 | 24 | ![](./screenshots/example-pr.png) 25 | 26 | this will show the check as pending as only 1 of the tasks is completed s (same if none etc.): 27 | 28 | ![](./screenshots/tasks-remaining.png) 29 | 30 | Once all tasks are marked off it'll show as completed: 31 | 32 | ![](./screenshots/tasks-completed.png) 33 | 34 | 35 | Also when viewing all Pull Requests, you'll see the green tick when all completed: 36 | ![](./screenshots/success-pr.png) 37 | 38 | & an orange dot when still tasks todo: 39 | ![](./screenshots/pending-pr.png) 40 | 41 | & if you have other CI tests such as unit tests etc, our pending status will not get in the way of failing tests, they will still show as the red cross: 42 | ![](./screenshots/failing-pr.png) 43 | 44 | 45 | Find out more about GitHub task lists: https://help.github.com/en/articles/about-task-lists 46 | 47 |
48 | 49 | Powered By DO 50 | 51 | 52 | This project is supported by & hosted on Digital Ocean, thanks! 53 | 54 | ## Docs 55 | 56 | Install & add to the repos you want. 57 | 58 | Want to require tasks to be complete before it can be merged? 59 | 60 | Inside your GitHub repo > Settings > Branches > Branch protection rules > Add rule > select require checks & require this check to pass. 61 | 62 | By default, we mark the check as in_progress until all tasks pass and then it marks it as successful. 63 | 64 | ## Skippable tasks 65 | 66 | Tasks that contain "POST-MERGE" or "N/A" in all caps are skipped. This is useful for tasks that are not applicable to the PR, or tasks that are only applicable after the PR is merged. 67 | This was inspired by [another project here](https://github.com/Shopify/task-list-checker/tree/main?tab=readme-ov-file#in-a-pull-request). 68 | 69 | ## Optional tasks 70 | 71 | Tasks that contain "OPTIONAL" in all caps are also skipped unless checked, they are also added to an "(+X optional)" text in the check. This is useful for tasks that are not required to be completed before the PR can be merged. 72 | 73 | ## Contributing / Development 74 | 75 | Code previous ran on Glitch, now it's hosted on Digital Ocean. 76 | Hosting is via multiple droplets, one configured as a load balancer & then additional worker nodes/droplets for the actual checks to run on. 77 | 78 | *For previous glitch deployments, on the glitch page, click tools > console and then run `git pull origin master && refresh`. 79 | Permission changes would need to be changed in the app on github.* 80 | 81 | ### Local development 82 | 83 | Using node v18+ & npm 10+ (older versions may also work, your mileage may vary). 84 | 85 | Local development can be done by cloning this repo: 86 | ```bash 87 | git clone https://github.com/stilliard/github-task-list-completed.git 88 | cd github-task-list-completed 89 | ``` 90 | 91 | Setup up an App inside GitHub: 92 | https://github.com/settings/apps/ 93 | 94 | Install & run: 95 | 96 | ```bash 97 | npm install 98 | npm run dev 99 | ``` 100 | 101 | Load up http://localhost:3000/probot and follow intructions to set up the app. 102 | On first run, it will create your `.env` with an initial `WEBHOOK_PROXY_URL=xxxxx`. 103 | After following the set up, make sure you have `APP_ID`, `PRIVATE_KEY`, `WEBHOOK_SECRET` all set. 104 | You can change the port it runs on with `PORT=3001` for example and set a `NODE_ENV=production` for production logs, [more details about logs here](https://probot.github.io/docs/logging/). 105 | 106 | 107 | View full Probot docs here. 108 |
109 | 110 |
111 | 112 |
113 | 114 | Testing: 115 | ```bash 116 | npm test 117 | ``` 118 | 119 | For production: 120 | ```bash 121 | npm install --omit=dev 122 | npm start 123 | ``` 124 | 125 | Instead of `npm start`, typically in production you'll use a [service file to run via systemd](https://www.freedesktop.org/software/systemd/man/latest/systemd.syntax.html) or similar. 126 | e.g. 127 | ```systemd 128 | [Service] 129 | ExecStart=npm start 130 | WorkingDirectory=/srv/github-task-list-completed 131 | Restart=always 132 | 133 | [Install] 134 | WantedBy=multi-user.target 135 | ``` 136 | 137 | You can also help support the hosting and development of this project with coffee power: 138 | 139 | Buy Me A Coffee 140 | 141 | ## Security 142 | 143 | All code is Open source, [MIT license](./LICENSE). Production checks currently log repo name & PR ID for debug purposes only and logs are removed after a max of 7 days. 144 | No logs are recorded about your repos themselves nor the pull request contents. 145 | 146 | Hosted check is on DO's [SFO3](https://www.digitalocean.com/blog/introducing-a-new-datacenter-in-the-san-francisco-region-sfo3). 147 | 148 | If you discover a security issue please email it to myself at andrew@stapps.io and I will get back to you asap. For all other issues or help you can create an issue on this project - Thank you. 149 | 150 | ## Credits 151 | 152 | - [Probot](https://github.com/probot/probot) - Used to build this project 153 | - [Glitch](https://glitch.com/) - Previously used to start this project 154 | - [WIP](https://github.com/wip/app) - Inspiration for this project 155 | - [Juliia Osadcha / iconfinder](https://www.iconfinder.com/icons/1790658/checklist_checkmark_clipboard_document_list_tracklist_icon) Icon used for this project 156 | - [DigitalOcean](https://m.do.co/c/60c76a17a70d) - Hosting of the live app check 157 | -------------------------------------------------------------------------------- /app.yml: -------------------------------------------------------------------------------- 1 | # This is a GitHub App Manifest. These settings will be used by default when 2 | # initially configuring your GitHub App. 3 | # 4 | # NOTE: changing this file will not update your GitHub App settings. 5 | # You must visit github.com/settings/apps/your-app-name to edit them. 6 | # 7 | # Read more about configuring your GitHub App: 8 | # https://probot.github.io/docs/development/#configuring-a-github-app 9 | # 10 | # Read more about GitHub App Manifests: 11 | # https://developer.github.com/apps/building-github-apps/creating-github-apps-from-a-manifest/ 12 | 13 | # The list of events the GitHub App subscribes to. 14 | # Uncomment the event names below to enable them. 15 | default_events: 16 | - check_run 17 | # - check_suite 18 | # - commit_comment 19 | # - create 20 | # - delete 21 | # - deployment 22 | # - deployment_status 23 | # - fork 24 | # - gollum 25 | - issue_comment 26 | # - issues 27 | # - label 28 | # - milestone 29 | # - member 30 | # - membership 31 | # - org_block 32 | # - organization 33 | # - page_build 34 | # - project 35 | # - project_card 36 | # - project_column 37 | # - public 38 | - pull_request 39 | - pull_request_review 40 | - pull_request_review_comment 41 | # - push 42 | # - release 43 | # - repository 44 | # - repository_import 45 | # - status 46 | # - team 47 | # - team_add 48 | # - watch 49 | 50 | # The set of permissions needed by the GitHub App. The format of the object uses 51 | # the permission name for the key (for example, issues) and the access type for 52 | # the value (for example, write). 53 | # Valid values are `read`, `write`, and `none` 54 | default_permissions: 55 | # Repository creation, deletion, settings, teams, and collaborators. 56 | # https://developer.github.com/v3/apps/permissions/#permission-on-administration 57 | # administration: read 58 | 59 | # Checks on code. 60 | # https://developer.github.com/v3/apps/permissions/#permission-on-checks 61 | checks: write 62 | 63 | # Repository contents, commits, branches, downloads, releases, and merges. 64 | # https://developer.github.com/v3/apps/permissions/#permission-on-contents 65 | # contents: read 66 | 67 | # Deployments and deployment statuses. 68 | # https://developer.github.com/v3/apps/permissions/#permission-on-deployments 69 | # deployments: read 70 | 71 | # Issues and related comments, assignees, labels, and milestones. 72 | # https://developer.github.com/v3/apps/permissions/#permission-on-issues 73 | issues: read 74 | 75 | # Search repositories, list collaborators, and access repository metadata. 76 | # https://developer.github.com/v3/apps/permissions/#metadata-permissions 77 | # metadata: read 78 | 79 | # Retrieve Pages statuses, configuration, and builds, as well as create new builds. 80 | # https://developer.github.com/v3/apps/permissions/#permission-on-pages 81 | # pages: read 82 | 83 | # Pull requests and related comments, assignees, labels, milestones, and merges. 84 | # https://developer.github.com/v3/apps/permissions/#permission-on-pull-requests 85 | pull_requests: read 86 | 87 | # Manage the post-receive hooks for a repository. 88 | # https://developer.github.com/v3/apps/permissions/#permission-on-repository-hooks 89 | # repository_hooks: read 90 | 91 | # Manage repository projects, columns, and cards. 92 | # https://developer.github.com/v3/apps/permissions/#permission-on-repository-projects 93 | # repository_projects: read 94 | 95 | # Retrieve security vulnerability alerts. 96 | # https://developer.github.com/v4/object/repositoryvulnerabilityalert/ 97 | # vulnerability_alerts: read 98 | 99 | # Commit statuses. 100 | # https://developer.github.com/v3/apps/permissions/#permission-on-statuses 101 | # statuses: read 102 | 103 | # Organization members and teams. 104 | # https://developer.github.com/v3/apps/permissions/#permission-on-members 105 | # members: read 106 | 107 | # View and manage users blocked by the organization. 108 | # https://developer.github.com/v3/apps/permissions/#permission-on-organization-user-blocking 109 | # organization_user_blocking: read 110 | 111 | # Manage organization projects, columns, and cards. 112 | # https://developer.github.com/v3/apps/permissions/#permission-on-organization-projects 113 | # organization_projects: read 114 | 115 | # Manage team discussions and related comments. 116 | # https://developer.github.com/v3/apps/permissions/#permission-on-team-discussions 117 | # team_discussions: read 118 | 119 | # Manage the post-receive hooks for an organization. 120 | # https://developer.github.com/v3/apps/permissions/#permission-on-organization-hooks 121 | # organization_hooks: read 122 | 123 | # Get notified of, and update, content references. 124 | # https://developer.github.com/v3/apps/permissions/ 125 | # organization_administration: read 126 | 127 | 128 | # The name of the GitHub App. Defaults to the name specified in package.json 129 | name: GH Task List Completed - DEV BUILD 130 | 131 | # The homepage of your GitHub App. 132 | url: https://github.com/stilliard/github-task-list-completed 133 | 134 | # A description of the GitHub App. 135 | description: Development only version of the task list completed app, experimental, please use the public version 136 | 137 | # Set to true when your GitHub App is available to the public or false when it is only accessible to the owner of the app. 138 | # Default: true 139 | public: false 140 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const checkOutstandingTasks = require('./src/check-outstanding-tasks'); 2 | 3 | const ENABLE_ID_LOGS = true; // Repo name & ID only for logs, no private data logged! (Repo name only needed to help with issue reports & debugging). 4 | 5 | module.exports = (app) => { 6 | app.log('Yay! The app was loaded!'); 7 | 8 | // watch for pull requests & their changes 9 | app.on([ 10 | 'pull_request.opened', 11 | 'pull_request.edited', 12 | 'pull_request.synchronize', 13 | 'issue_comment', // for comments on GitHub issues 14 | 'pull_request_review', // reviews 15 | 'pull_request_review_comment', // comment lines on diffs for reviews 16 | ], async context => { 17 | 18 | // lookup the pr 19 | let pr = context.payload.pull_request; 20 | 21 | // check if this is an issue rather than pull event 22 | if (context.name == 'issue_comment' && ! pr) { 23 | // if so we need to make sure this is for a PR only 24 | if (! context.payload.issue.pull_request) { 25 | return; 26 | } 27 | // & lookup the PR it's for to continue 28 | try { 29 | let response = await context.octokit.pulls.get(context.repo({ 30 | pull_number: context.payload.issue.number 31 | })); 32 | pr = response.data; 33 | // cleanup 34 | response = null; 35 | } catch (err) { 36 | context.log.error(`Error looking up PR, skipping. Error (${err.status}): ${err.message}`); 37 | } 38 | } 39 | if (! pr) { 40 | context.log.error(`Not on a PR? Skipping. context.name: ${context.name}`); 41 | return; 42 | } 43 | 44 | // pr details 45 | let prRepo = pr.head.repo.full_name; 46 | let prNumber = pr.number; 47 | let prHeadSha = pr.head.sha; 48 | let prBody = pr.body; 49 | let prUser = pr.user.login; 50 | // cleanup 51 | pr = null; 52 | 53 | // log helper 54 | function log(message, type = 'info') { 55 | if (ENABLE_ID_LOGS) { 56 | context.log[type](`PR ${prRepo}#${prNumber}: ${message}`); 57 | } 58 | } 59 | 60 | log(`Request received [Context: ${context.id}]`); 61 | 62 | // if the author is a renovate bot, ignore checks 63 | // https://www.mend.io/free-developer-tools/renovate/ 64 | if (prUser.indexOf('renovate[bot]') !== -1) { 65 | prBody = null; 66 | } 67 | 68 | let outstandingTasks = checkOutstandingTasks(prBody); 69 | 70 | 71 | // lookup comments on the PR 72 | let comments; 73 | try { 74 | comments = await context.octokit.issues.listComments(context.repo({ 75 | per_page: 100, 76 | issue_number: prNumber 77 | })); 78 | 79 | // bots to ignore 80 | let bots = [ 81 | 'linear', // ref https://github.com/stilliard/github-task-list-completed/issues/33 82 | 'linear[bot]', 83 | ]; 84 | // filter out comments from the bot 85 | comments.data = comments.data.filter(comment => { 86 | return ! bots.includes(comment.user.login); 87 | }); 88 | // cleanup 89 | bots = null; 90 | 91 | } catch (err) { 92 | if (err.status === 403) { // if we don't have access to the repo, skip entirely 93 | log(`No access, skipping entirely. Error (${err.status}): ${err.message}`, 'error'); 94 | return; 95 | } 96 | log(`Error looking up comments, skipping. Error (${err.status}): ${err.message}`, 'error'); 97 | } 98 | 99 | log('Main comments api lookup complete'); 100 | 101 | // as well as review comments 102 | let reviewComments; 103 | try { 104 | reviewComments = await context.octokit.pulls.listReviews(context.repo({ 105 | per_page: 100, 106 | pull_number: prNumber 107 | })); 108 | if (reviewComments.data.length) { 109 | comments.data = comments.data.concat(reviewComments.data); 110 | } 111 | // cleanup 112 | reviewComments = null; 113 | } catch (err) { 114 | log(`Error looking up review comments, skipping. Error (${err.status}): ${err.message}`, 'error'); 115 | } 116 | 117 | log('Review comments api lookup complete'); 118 | 119 | // and diff level comments on reviews 120 | try { 121 | let reviewDiffComments = await context.octokit.pulls.listReviewComments(context.repo({ 122 | per_page: 100, 123 | pull_number: prNumber 124 | })); 125 | if (reviewDiffComments.data.length) { 126 | comments.data = comments.data.concat(reviewDiffComments.data); 127 | } 128 | // cleanup 129 | reviewDiffComments = null; 130 | } catch (err) { 131 | log(`Error looking up review diff comments, skipping. Error (${err.status}): ${err.message}`, 'error'); 132 | } 133 | 134 | log('Diff comments api lookup complete'); 135 | 136 | // & check them for tasks 137 | if (comments && comments.data && comments.data.length) { 138 | comments.data.forEach(function (comment) { 139 | let commentOutstandingTasks = checkOutstandingTasks(comment.body); 140 | outstandingTasks.total += commentOutstandingTasks.total; 141 | outstandingTasks.remaining += commentOutstandingTasks.remaining; 142 | outstandingTasks.optionalTotal += commentOutstandingTasks.optionalTotal; 143 | outstandingTasks.optionalRemaining += commentOutstandingTasks.optionalRemaining; 144 | outstandingTasks.tasks = (outstandingTasks.tasks || []).concat(commentOutstandingTasks.tasks || []); 145 | outstandingTasks.optionalTasks = (outstandingTasks.optionalTasks || []).concat(commentOutstandingTasks.optionalTasks || []); 146 | }); 147 | } 148 | 149 | // optional addon text 150 | let optionalText = ''; 151 | if (outstandingTasks.optionalRemaining > 0) { 152 | optionalText = ' (+' + outstandingTasks.optionalRemaining + ' optional)'; 153 | } 154 | 155 | // make a markdown table of the tasks 156 | let tasksTable = ''; 157 | if (outstandingTasks.total > 0) { 158 | tasksTable += ` 159 | ## Required Tasks 160 | | Task | Status | 161 | | ---- | ------ | 162 | ${outstandingTasks.tasks.map(task => `| ${task.task} | ${task.status} |`).join('\n')} 163 | `; 164 | } 165 | if (outstandingTasks.optionalTotal > 0) { 166 | tasksTable += ` 167 | ## Optional Tasks 168 | | Task | Status | 169 | | ---- | ------ | 170 | ${outstandingTasks.optionalTasks.map(task => `| ${task.task} | ${task.status} |`).join('\n')} 171 | `; 172 | } 173 | 174 | let check = { 175 | name: 'task-list-completed', 176 | head_branch: '', 177 | head_sha: prHeadSha, 178 | started_at: (new Date).toISOString(), 179 | status: 'in_progress', 180 | output: { 181 | title: (outstandingTasks.total - outstandingTasks.remaining) + ' / ' + outstandingTasks.total + ' tasks completed' + optionalText, 182 | summary: outstandingTasks.remaining + ' task' + (outstandingTasks.remaining > 1 ? 's' : '') + ' still to be completed' + optionalText, 183 | text: tasksTable 184 | }, 185 | request: { 186 | // timeout the request after 3 minutes 187 | timeout: 1000 * 60 * 3, 188 | // retry up to 10 times on request timeouts 189 | retries: 10, 190 | retryAfter: 10, // wait 10 seconds 191 | }, 192 | }; 193 | 194 | // all finished? 195 | if (outstandingTasks.remaining === 0) { 196 | check.status = 'completed'; 197 | check.conclusion = 'success'; 198 | check.completed_at = (new Date).toISOString(); 199 | check.output.summary = 'All tasks have been completed' + optionalText; 200 | }; 201 | 202 | log('Complete and sending back to GitHub'); 203 | 204 | // cleanup 205 | prBody = null; 206 | outstandingTasks = null; 207 | comments = null; 208 | tasksTable = null; 209 | optionalText = null; 210 | 211 | // send check back to GitHub 212 | try { 213 | const response = await context.octokit.checks.create(context.repo(check)); 214 | log(`Check response status from GitHub ${response.status} [X-GitHub-Request-Id: ${response.headers['x-github-request-id']}]`); 215 | } catch (err) { 216 | log(`Error sending check back to GitHub. Error (${err.status}): ${err.message}`, 'error'); 217 | } 218 | 219 | return; 220 | }); 221 | }; 222 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "//1": "describes your app and its dependencies", 3 | "//2": "https://docs.npmjs.com/files/package.json", 4 | "//3": "updating this file will download and update your packages", 5 | "name": "task-list-completed", 6 | "version": "1.0.0", 7 | "description": "Check a pull requests task list is complete", 8 | "author": "stilliard ", 9 | "license": "ISC", 10 | "repository": "https://github.com/stilliard/github-task-list-completed.git", 11 | "scripts": { 12 | "dev-debug": "nodemon --exec \"LOG_LEVEL=debug npm start\"", 13 | "dev": "nodemon --exec \"npm start\"", 14 | "start": "probot run ./index.js", 15 | "test": "jest" 16 | }, 17 | "dependencies": { 18 | "marked": "^9.1.2", 19 | "probot": "^12.3.1" 20 | }, 21 | "devDependencies": { 22 | "jest": "^29.7.0", 23 | "smee-client": "1.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /screenshots/example-pr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stilliard/github-task-list-completed/18b23a24a2ad63d9ad1e7f522811040ff80ad2bd/screenshots/example-pr.png -------------------------------------------------------------------------------- /screenshots/failing-pr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stilliard/github-task-list-completed/18b23a24a2ad63d9ad1e7f522811040ff80ad2bd/screenshots/failing-pr.png -------------------------------------------------------------------------------- /screenshots/pending-pr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stilliard/github-task-list-completed/18b23a24a2ad63d9ad1e7f522811040ff80ad2bd/screenshots/pending-pr.png -------------------------------------------------------------------------------- /screenshots/success-pr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stilliard/github-task-list-completed/18b23a24a2ad63d9ad1e7f522811040ff80ad2bd/screenshots/success-pr.png -------------------------------------------------------------------------------- /screenshots/tasks-completed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stilliard/github-task-list-completed/18b23a24a2ad63d9ad1e7f522811040ff80ad2bd/screenshots/tasks-completed.png -------------------------------------------------------------------------------- /screenshots/tasks-remaining.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stilliard/github-task-list-completed/18b23a24a2ad63d9ad1e7f522811040ff80ad2bd/screenshots/tasks-remaining.png -------------------------------------------------------------------------------- /src/check-outstanding-tasks.js: -------------------------------------------------------------------------------- 1 | const marked = require('marked'); 2 | 3 | module.exports = function (body) { 4 | if (body === null) { 5 | return { 6 | total: 0, 7 | remaining: 0 8 | }; 9 | } 10 | 11 | let tokens = marked.lexer(body, { gfm: true }); 12 | // flatten the nested tokens to make filtering easier 13 | let allTokens = tokens.flatMap(function mapper(token) { 14 | if (token.tokens && token.tokens.length > 1) { 15 | return token.tokens.flatMap(mapper); 16 | } 17 | 18 | return token.items && token.items.length ? token.items.flatMap(mapper) : [token]; 19 | }); 20 | // and filter down to just the task list items 21 | let listItems = allTokens.filter(token => token.type === 'list_item'); 22 | let optionalItems = listItems.filter(item => item.text.indexOf('OPTIONAL') !== -1); 23 | 24 | // filter out skippable items, case sensitive 25 | let skippable = [ 26 | 'POST-MERGE', 27 | 'N/A', 28 | 'OPTIONAL', // this is a special case, we want to count these items but not include them in the remaining count 29 | ]; 30 | listItems = listItems.filter(item => { 31 | return ! skippable.some(skip => item.text.indexOf(skip) !== -1) || item.text.indexOf('OPTIONAL') !== -1 && item.checked === true; 32 | }); 33 | 34 | // return counts of task list items and how many are left to be completed 35 | return { 36 | tasks: listItems.filter(item => item.checked !== undefined && item.text.indexOf('OPTIONAL') === -1).map(item => { 37 | return { 38 | task: item.text.replace(/\[x\]|\[ \]/, '').trim(), 39 | status: item.checked ? 'Completed' : '**Incomplete**', 40 | }; 41 | }), 42 | optionalTasks: optionalItems.filter(item => item.checked !== undefined).map(item => { 43 | return { 44 | task: item.text.replace(/\[x\]|\[ \]/, '').trim(), 45 | status: item.checked ? 'Completed' : '**Incomplete**', 46 | }; 47 | }), 48 | total: listItems.filter(item => item.checked !== undefined).length, 49 | remaining: listItems.filter(item => item.checked === false).length, 50 | optionalTotal: optionalItems.filter(item => item.checked !== undefined).length, 51 | optionalRemaining: optionalItems.filter(item => item.checked === false).length 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | // Tests use Jest by default. 2 | // To read more about testing with Probot, visit https://probot.github.io/docs/testing/ 3 | 4 | const checkOutstandingTasks = require('../src/check-outstanding-tasks'); 5 | test('Test outstanding tasks found', () => { 6 | let markdown = ` 7 | Hello World 8 | - [ ] testing 9 | - [x] 123 10 | `; 11 | let results = checkOutstandingTasks(markdown); 12 | expect(results.total).toBe(2); 13 | expect(results.remaining).toBe(1); 14 | }); 15 | 16 | test('Test no outstanding tasks', () => { 17 | let markdown = ` 18 | Hello World 19 | - [x] testing 20 | - [x] 123 21 | `; 22 | let results = checkOutstandingTasks(markdown); 23 | expect(results.total).toBe(2); 24 | expect(results.remaining).toBe(0); 25 | }); 26 | 27 | test('Test no tasks', () => { 28 | let markdown = ` 29 | Hello World 30 | `; 31 | let results = checkOutstandingTasks(markdown); 32 | expect(results.total).toBe(0); 33 | expect(results.remaining).toBe(0); 34 | }); 35 | 36 | test('Test dont count normal lists', () => { 37 | let markdown = ` 38 | Hello World 39 | - normal 40 | - [x] task 1 41 | - [ ] task 2 42 | `; 43 | let results = checkOutstandingTasks(markdown); 44 | expect(results.total).toBe(2); 45 | expect(results.remaining).toBe(1); 46 | }); 47 | 48 | test('Test nested lists', () => { 49 | let markdown = ` 50 | Hello World 51 | - [x] normal 52 | - section1 53 | - [x] task 1-1 54 | - [ ] task 1-2 55 | - section2 56 | - [ ] task 2-1 57 | - [x] task 2-2 58 | `; 59 | let results = checkOutstandingTasks(markdown); 60 | expect(results.total).toBe(5); 61 | expect(results.remaining).toBe(2); 62 | }); 63 | 64 | test('Test skip items', () => { 65 | let markdown = ` 66 | Hello World 67 | - normal 68 | - [ ] task one 69 | - [ ] POST-MERGE: abc 70 | - [ ] this is not a post-merge test 71 | - [ ] N/A skipped 72 | - [x] n/a not skipped 73 | `; 74 | let results = checkOutstandingTasks(markdown); 75 | expect(results.total).toBe(3); 76 | expect(results.remaining).toBe(2); 77 | }); 78 | 79 | test('Test optional items', () => { 80 | let markdown = ` 81 | Hello World 82 | - normal 83 | - [x] OPTIONAL: one 84 | - [ ] OPTIONAL: two 85 | - [x] this is not an optional test 86 | `; 87 | let results = checkOutstandingTasks(markdown); 88 | expect(results.total).toBe(2); 89 | expect(results.remaining).toBe(0); 90 | expect(results.optionalRemaining).toBe(1); 91 | }); 92 | --------------------------------------------------------------------------------