├── CONTRIBUTING.md ├── LICENSE ├── .gitignore ├── package.json ├── action.yml ├── README.md └── index.js /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contribute to this project! 2 | 3 | To get started contributing to this project: 4 | 5 | 1. Clone this repository. 6 | 2. Run `npm install` in your local checkout. 7 | 8 | To build this project (generate `/dist` files): 9 | 1. Run `npm run build`. 10 | 2. Optionally lint your files by running `npm run lint`. 11 | 12 | There is a pre-commit hook configured to automatically build the `dist` directory and a pre-push hook that automatically runs `npm run lint`. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 GitHub Actions 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | # Editors 4 | .vscode 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Other Dependency directories 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # next.js build output 65 | .next 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javascript-action", 3 | "version": "1.0.0", 4 | "description": "JavaScript Action Template", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "standard", 8 | "lint-fix": "standard --fix", 9 | "build": "ncc build index.js", 10 | "test": "npm run lint" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/actions/javascript-action.git" 15 | }, 16 | "keywords": [ 17 | "GitHub", 18 | "Actions", 19 | "JavaScript" 20 | ], 21 | "author": "GitHub", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/actions/javascript-action/issues" 25 | }, 26 | "homepage": "https://github.com/actions/javascript-action#readme", 27 | "dependencies": { 28 | "@actions/core": "^1.1.1", 29 | "@actions/github": "^2.2.0", 30 | "@octokit/graphql": "^4.5.3" 31 | }, 32 | "devDependencies": { 33 | "@zeit/ncc": "^0.22.3", 34 | "husky": "^4.2.5", 35 | "standard": "^14.3.4" 36 | }, 37 | "standard": { 38 | "ignore": [ 39 | "dist/", 40 | "tests/" 41 | ] 42 | }, 43 | "husky": { 44 | "hooks": { 45 | "pre-commit": "npm run build && git add dist/", 46 | "pre-push": "npm run lint" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'First responder' 2 | description: 'Catch team pings from issues and pull requests outside of your repository' 3 | inputs: 4 | token: 5 | description: 'An access token.' 6 | required: true 7 | team: # id of input 8 | description: 'The team ping to search for that is part of the `org` specified below. Do not include the org name (for example, use `docs-content-ecosystem`). Issues and pull requests authored or commented on by members of `team` are ignored unless you specify an alternate `ignore-team` parameter.' 9 | required: true 10 | org: 11 | description: 'The organization where the action should search for issues and pull requests.' 12 | required: true 13 | since: 14 | description: 'The start date to search for team pings. The action searches for issues or pull requests created since the date specified. Form: {4 digit year}-{month}-{day}. For example: "2020-5-20"' 15 | required: false 16 | default: '2020-1-1' 17 | project-board: 18 | description: 'The URL of the project board to place issues and pull requests. Must be an org project board.' 19 | required: true 20 | project-column: 21 | description: 'The id of the column to add issues and pull requests.' 22 | required: true 23 | ignore-team: 24 | description: 'Ignores issues and pull requests authored or commented on by members of this team. Issues and pull requests authored or commented on by members of `team` are ignored unless you specify an alternate `ignore-team` parameter. You can use `ignore-team` to specify a larger team or a team that does not match the team ping being searched. The value you specify for `ignore-team` overrides the `team` value.' 25 | required: false 26 | include-repos: 27 | description: 'Repositories to include when searching issues and pull requests. You can add more than one repository by using a comma-separated list. Format: {owner}/{repo}. For example: "octocat/hello-world, octocat/foobar"' 28 | required: false 29 | ignore-repos: 30 | description: 'Repositories to ignore when searching issues and pull requests. You can add more than one repository by using a comma-separated list. Format: {owner}/{repo}. For example: "octocat/hello-world, octocat/foobar"' 31 | required: false 32 | ignore-authors: 33 | description: 'Ignores issues and pull requests authored by these accounts. You can add more than one repository by using a comma-separated list (for example, "actions-bot, hubot")' 34 | required: false 35 | ignore-commenters: 36 | description: 'Ignores issues and pull requests commented by thee accounts. You can add more than one repository by using a comma-separated list (for example, "actions-bot, hubot")' 37 | required: false 38 | ignore-labels: 39 | description: 'Ignores issues and pull requests with specific labels. You can add more than one label by using a comma-separated list (for example, "space-2x, autogen")' 40 | required: false 41 | comment-body: 42 | description: 'A comment added to the issue or pull request.' 43 | required: false 44 | 45 | runs: 46 | using: 'node12' 47 | main: 'dist/index.js' 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # First responder 2 | 3 | This action searches for team pings in issues and pull requests in an organization. The issues and pull requests are added to an organization project board. You can add a comment to the issue or pull request letting people know that your team will first respond soon. 4 | 5 | # Input parameters 6 | 7 | - `token`: **Required:** An access token. 8 | - `team`: **Required:** The team ping to search for that is part of the `org` specified below. Do not include the org name (for example, use `docs-content-ecosystem`). Issues and pull requests authored or commented on by members of `team` are ignored unless you specify an alternate `ignore-team` parameter. 9 | - `org`: **Required:** The organization where the action should search for issues and pull requests. 10 | - `since`: The start date to search for team pings. The action searches for issues or pull requests created since the date specified. Form: {4 digit year}-{month}-{day}. For example: '2020-5-20' 11 | - `project-board`: The URL of the project board to place issues and pull requests. Must be an org project board. 12 | - `project-column`: The id of the column to add issues and pull requests. 13 | - `ignore-team`: Ignores issues and pull requests authored or commented on by members of this team. Issues and pull requests authored or commented on by members of `team` are ignored unless you specify an alternate `ignore-team` parameter. You can use `ignore-team` to specify a larger team or a team that does not match the team ping being searched. The value you specify for `ignore-team` overrides the `team` value. 14 | - `include-repos`: Repositories to include when searching issues and pull requests. You can add more than one repository by using a comma-separated list. Format: {owner}/{repo}. For example: 'octocat/hello-world, octocat/foobar'. Note: This cannot be used with `ignore-repos`. 15 | - `ignore-repos`: Repositories to ignore when searching issues and pull requests. You can add more than one repository by using a comma-separated list. Format: {owner}/{repo}. For example: 'octocat/hello-world, octocat/foobar'. Note: This cannot be used with `include-repos`. 16 | - `ignore-authors`: Ignores issues and pull requests authored by these accounts. You can add more than one repository by using a comma-separated list (for example, 'actions-bot, hubot') 17 | - `ignore-commenters`: Ignores issues and pull requests commented by thee accounts. You can add more than one repository by using a comma-separated list (for example, 'actions-bot, hubot') 18 | - `comment-body`: A comment added to the issue or pull request. 19 | 20 | ## `token` 21 | 22 | To read and write organization project boards, you need to use an access token with `repo` and `write:org` access. Ensure that you add the user that owns the access token to the project board with admin permission. For help creating an access token or managing project board members see the GitHub docs: 23 | - [Creating a personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token#creating-a-token) 24 | - [Managing team access to an organization project board](https://docs.github.com/en/github/setting-up-and-managing-organizations-and-teams/managing-team-access-to-an-organization-project-board) 25 | - [Managing an individual's access to an organization project board](https://docs.github.com/en/github/setting-up-and-managing-organizations-and-teams/managing-an-individuals-access-to-an-organization-project-board) 26 | 27 | 28 | ## Example workflow 29 | 30 | This workflow includes two team pings and runs every hour and can also be run manually. 31 | 32 | ```yml 33 | name: First responder triage 34 | on: 35 | workflow_dispatch: 36 | schedule: 37 | - cron: '0 * * * *' 38 | 39 | jobs: 40 | first-responder-product: 41 | name: Spacely product team pings 42 | runs-on: ubuntu-latest 43 | 44 | steps: 45 | - name: Get team pings for Spacely product division 46 | uses: rachmari/first-responder@v1.0.0 47 | with: 48 | token: ${{secrets.FR_ACCESS_TOKEN}} 49 | team: 'spacely-product' 50 | org: 'spacelysprocketsinc' 51 | since: '2020-08-05' 52 | project-board: 'https://github.com/orgs/spacelysprocketsinc/projects/1' 53 | project-column: 9 54 | ignore-repos: 'spacelysprocketsinc/product-spacely, spacelysprocketsinc/product-spacely-sprockets' 55 | ignore-authors: 'sprocketbot, github-actions' 56 | ignore-commenters: 'sprocketbot' 57 | comment-body: ':rocket: Thanks for the ping! :bellhop_bell: This issue was added to our first-responder project board. A team member will be along shortly to review this issue.' 58 | 59 | first-responder-product-subteam: 60 | name: Spacely product sprockets pings 61 | runs-on: ubuntu-latest 62 | 63 | steps: 64 | - name: Get team pings for Spacely sprockets product division 65 | uses: rachmari/first-responder@v1.0.0 66 | with: 67 | token: ${{secrets.FR_ACCESS_TOKEN}} 68 | team: 'spacely-sprockets-product' 69 | org: 'spacelysprocketsinc' 70 | since: '2020-08-05' 71 | project-board: 'https://github.com/orgs/spacelysprocketsinc/projects/1' 72 | project-column: 10 73 | ignore-repos: 'spacelysprocketsinc/product-spacely, spacelysprocketsinc/product-spacely-sprockets' 74 | ignore-authors: 'sprocketbot, github-actions' 75 | ignore-commenters: 'sprocketbot' 76 | comment-body: ':robot: Thanks for the ping to team sprockets! :bellhop_bell: This issue was added to our first-responder project board. A team member will be along shortly to review this issue.' 77 | 78 | ``` 79 | 80 | # Would you like to contribute? 81 | 82 | To get started read the [Contributing](./CONTRIBUTING.md) docs. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { GitHub } = require('@actions/github') 2 | const core = require('@actions/core') 3 | 4 | async function run () { 5 | const token = core.getInput('token') 6 | const team = core.getInput('team') 7 | const org = core.getInput('org') 8 | const fullTeamName = `${org}/${team}` 9 | const since = core.getInput('since') !== '' 10 | ? core.getInput('since') : '2020-01-01' 11 | const projectBoard = core.getInput('project-board') 12 | const columnId = parseInt(core.getInput('project-column'), 10) 13 | const ignoreTeam = core.getInput('ignore-team') 14 | const body = core.getInput('comment-body') 15 | const includeRepos = core.getInput('include-repos') !== '' 16 | ? core.getInput('include-repos').split(',').map(x => x.trim()) : [] 17 | const ignoreRepos = core.getInput('ignore-repos') !== '' 18 | ? core.getInput('ignore-repos').split(',').map(x => x.trim()) : [] 19 | const ignoreLabels = core.getInput('ignore-labels') !== '' 20 | ? core.getInput('ignore-labels').split(',').map(x => x.trim()) : [] 21 | let ignoreAuthors = core.getInput('ignore-authors') !== '' 22 | ? core.getInput('ignore-authors').split(',').map(x => x.trim()) : [] 23 | let ignoreCommenters = core.getInput('ignore-commenters') !== '' 24 | ? core.getInput('ignore-commenters').split(',').map(x => x.trim()) : [] 25 | const octokit = new GitHub(token) 26 | 27 | const projectInfo = await getProjectMetaData(projectBoard, org) 28 | 29 | // Only include-repos OR ignore-repos can be passed in but not both. 30 | if (includeRepos.length > 0 && ignoreRepos.length > 0) { 31 | throw new Error('Pass either include-repos or ignore-repos as an option but not both.') 32 | } 33 | 34 | // Create a list of users to ignore in the search query 35 | let teamMembers = [] 36 | if (ignoreTeam === '') { 37 | teamMembers = await getTeamLogins(octokit, org, team) 38 | } else { 39 | teamMembers = await getTeamLogins(octokit, org, ignoreTeam) 40 | } 41 | ignoreAuthors = ignoreAuthors.concat(teamMembers) 42 | ignoreCommenters = ignoreCommenters.concat(teamMembers) 43 | 44 | // Assemble and run the issue/pull request search query 45 | const issues = await getTeamPingIssues(octokit, org, fullTeamName, ignoreAuthors, ignoreCommenters, since, projectInfo, includeRepos, ignoreRepos, ignoreLabels) 46 | 47 | if (issues.data.incomplete_results === false) { 48 | console.log('🌵🌵🌵 All search results were found. 🌵🌵🌵') 49 | } else { 50 | console.log('🐢 The search result indicated that results may not be complete. This doesn\'t necessarily mean that all results weren\'t returned. See https://docs.github.com/en/rest/reference/search#timeouts-and-incomplete-results for details.') 51 | } 52 | 53 | if (issues.data.items.length === 0) { 54 | return 'No new team pings. 💫🦄🌈🦩✨' 55 | } 56 | 57 | console.log(`🚨 Search query found ${issues.data.items.length} issues and prs. 🚨`) 58 | 59 | for (const issue of issues.data.items) { 60 | let [, , , owner, repo, contentType, number] = issue.html_url.split('/') 61 | contentType = contentType === 'issues' ? 'Issue' : 'PullRequest' 62 | await addProjectCard(octokit, owner, repo, number, contentType, columnId) 63 | 64 | if (body !== '') { 65 | const comment = await octokit.issues.createComment({ 66 | issue_number: number, 67 | owner: owner, 68 | repo: repo, 69 | body: body 70 | }) 71 | if (comment.status !== 201) { 72 | throw new Error(`Unable to create a comment in #${issue.html_url} - ${comment.status}.`) 73 | } 74 | } 75 | } 76 | return '🏁⛑' 77 | } 78 | 79 | async function getTeamPingIssues (octokit, org, team, authors, commenters, since = '2019-01-01', projectBoard, includeRepos, ignoreRepos, ignoreLabels) { 80 | // Search for open issues in repositories owned by `org` 81 | // and includes a team mention to `team` 82 | let query = `per_page=100&q=is%3Aopen+org%3A${org}+team%3A${team}` 83 | for (const author of authors) { 84 | query = query.concat(`+-author%3A${author}`) 85 | } 86 | for (const commenter of commenters) { 87 | query = query.concat(`+-commenter%3A${commenter}`) 88 | } 89 | 90 | // Add the created since date query 91 | query = query.concat(`+created%3A%3E${since}`) 92 | 93 | if (includeRepos.length > 0) { 94 | // Add include repos query 95 | includeRepos.forEach(elem => { 96 | query = query.concat(`+repo%3A${elem}`) 97 | }) 98 | } else if (ignoreRepos.length > 0) { 99 | // Add ignore repos query 100 | ignoreRepos.forEach(elem => { 101 | query = query.concat(`+-repo%3A${elem}`) 102 | }) 103 | } 104 | 105 | // Add ignore labels query 106 | ignoreLabels.forEach(elem => { 107 | query = query.concat(`+-label%3A${elem}`) 108 | }) 109 | 110 | // Ignore issues already on the project board 111 | const ref = projectBoard.repo !== undefined 112 | ? `${projectBoard.owner}%2F${projectBoard.repo}` : projectBoard.owner 113 | query = query.concat(`+-project%3A${ref}%2F${projectBoard.number}`) 114 | 115 | console.log(`🔎 Searh query 🔎 ${query}`) 116 | return await octokit.request(`GET /search/issues?${query}`) 117 | } 118 | 119 | async function getProjectMetaData (projectUrl, org) { 120 | const projectAttr = projectUrl.split('/') 121 | 122 | if (projectAttr[3] === 'orgs') { 123 | return { owner: projectAttr[4], number: parseInt(projectAttr[6], 10) } 124 | } else if (projectAttr[3] === org) { 125 | return { owner: projectAttr[3], number: parseInt(projectAttr[6], 10), repo: projectAttr[4] } 126 | } else { 127 | return console.log(`The project URL format is malformed and won't be included: ${projectUrl}`) 128 | } 129 | } 130 | 131 | async function addProjectCard (octokit, owner, repo, number, contentType, columnId) { 132 | let contentRef = '' 133 | if (contentType === 'Issue') { 134 | contentRef = await octokit.issues.get({ 135 | owner: owner, 136 | repo: repo, 137 | issue_number: number 138 | }) 139 | } else { 140 | contentRef = await octokit.pulls.get({ 141 | owner: owner, 142 | repo: repo, 143 | pull_number: number 144 | }) 145 | } 146 | const res = await octokit.projects.createCard({ 147 | column_id: columnId, 148 | content_id: contentRef.data.id, 149 | content_type: contentType 150 | }) 151 | 152 | if (res.status !== 201) { 153 | throw new Error(`Unable to create a project card for ${contentRef} - ${res.status}.`) 154 | } 155 | 156 | return console.log(`🔖 Successfully created a new card in column #${columnId} for ${contentType} #${number} from ${owner}/${repo}!`) 157 | } 158 | 159 | async function getTeamLogins (octokit, org, team) { 160 | const teamMembers = await octokit.teams.listMembersInOrg({ 161 | org: org, 162 | team_slug: team 163 | }) 164 | return teamMembers.data.map(member => member.login) 165 | } 166 | 167 | run() 168 | .then( 169 | (response) => { console.log(`Finished running: ${response}`) }, 170 | (error) => { 171 | console.log(`#ERROR# ${error}`) 172 | process.exit(1) 173 | } 174 | ) 175 | --------------------------------------------------------------------------------