├── .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 |
![]()
110 |
111 |
112 |
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