diff --git a/README.md b/README.md index a6cd237..af1f79a 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,11 @@ Set the following inputs in the workflow file ### `autolabel-config` -Provide the project column configuration for the auto labeling based on column name pattern matches. JSON object as string, example format [{"column":"in-progress"; "add-labels":["in-progress"]; "remove-labels":["triage"]}]. +Provide the project column configuration for the auto labeling based on column name pattern matches. JSON object as string, example format [{"column":"In progress", "add_labels":["in-progress"], "remove_labels":["triage"]}]. + +### `projectfilter-config` + +Provide the project patterns which will include/exclude projects. JSON object as string, example format {"include":["projectid"], "exclude":[]}. ## Outputs @@ -57,7 +61,7 @@ jobs: uses: Matticusau/projectcard-autolabel@v1.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - autolabel-config: '[{"column":"in-progress"; "add-labels":["in-progress"]; "remove-labels":["triage"]}]' + autolabel-config: '[{"column":"In progress", "add_labels":["in-progress"], "remove_labels":["triage"]}]' ``` > Note: The `uses` syntax includes tag/branch specification. For the latest release see [tags](https://github.com/Matticusau/projectcard-autolabel/tags). @@ -83,7 +87,7 @@ The action can be customized using the additional inputs on the workflow yaml fi ```yml with: repo-token: ${{ secrets.GITHUB_TOKEN }} - autolabel-config: '[{"column":"in-progress"; "add-labels":["in-progress"]; "remove-labels":["triage"]}]' + autolabel-config: '[{"column":"In progress", "add_labels":["in-progress"], "remove_labels":["triage"]}]' ``` ## Troubleshooting diff --git a/action.yml b/action.yml index 81d909f..f3050ca 100644 --- a/action.yml +++ b/action.yml @@ -8,9 +8,12 @@ inputs: autolabel-config: description: 'Provide the project column configuration for the auto labeling based on column name pattern matches. JSON object as string.' required: true + projectfilter-config: + description: 'Provide the project filter pattern configuration for the inclusions and exclusion of projects. JSON object as string.' + required: true runs: using: 'node12' main: 'lib/index.js' branding: - icon: git-merge + icon: sliders color: green \ No newline at end of file diff --git a/docs/ProjectCards.md b/docs/ProjectCards.md index e171408..7b831de 100644 --- a/docs/ProjectCards.md +++ b/docs/ProjectCards.md @@ -104,3 +104,179 @@ This was investigated in [Issue #35](https://github.com/Matticusau/pr-helper/iss } ] ``` + + +Sample Payload + +```json +{ + "payload": { + "action": "moved", + "changes": { + "column_id": { + "from": 12821424 + } + }, + "project_card": { + "after_id": null, + "archived": false, + "column_id": 12821425, + "column_url": "https://api.github.com/projects/columns/12821425", + "content_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/issues/23", + "created_at": "2021-02-07T11:50:32Z", + "creator": { + "avatar_url": "https://avatars.githubusercontent.com/u/11083642?v=4", + "events_url": "https://api.github.com/users/Matticusau/events{/privacy}", + "followers_url": "https://api.github.com/users/Matticusau/followers", + "following_url": "https://api.github.com/users/Matticusau/following{/other_user}", + "gists_url": "https://api.github.com/users/Matticusau/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/Matticusau", + "id": 11083642, + "login": "Matticusau", + "node_id": "MDQ6VXNlcjExMDgzNjQy", + "organizations_url": "https://api.github.com/users/Matticusau/orgs", + "received_events_url": "https://api.github.com/users/Matticusau/received_events", + "repos_url": "https://api.github.com/users/Matticusau/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/Matticusau/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Matticusau/subscriptions", + "type": "User", + "url": "https://api.github.com/users/Matticusau" + }, + "id": 54410237, + "node_id": "MDExOlByb2plY3RDYXJkNTQ0MTAyMzc=", + "note": null, + "project_url": "https://api.github.com/projects/11613902", + "updated_at": "2021-02-07T12:13:09Z", + "url": "https://api.github.com/projects/columns/cards/54410237" + }, + "repository": { + "archive_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/{archive_format}{/ref}", + "archived": false, + "assignees_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/assignees{/user}", + "blobs_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/branches{/branch}", + "clone_url": "https://github.com/Matticusau/pr-helper-demo.git", + "collaborators_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/comments{/number}", + "commits_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/commits{/sha}", + "compare_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/contents/{+path}", + "contributors_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/contributors", + "created_at": "2020-06-18T21:32:34Z", + "default_branch": "main", + "deployments_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/deployments", + "description": "Repository for demo and testing of https://github.com/Matticusau/pr-helper. Experimentation of features welcomed.", + "disabled": false, + "downloads_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/downloads", + "events_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/events", + "fork": false, + "forks": 0, + "forks_count": 0, + "forks_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/forks", + "full_name": "Matticusau/pr-helper-demo", + "git_commits_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/git/tags{/sha}", + "git_url": "git://github.com/Matticusau/pr-helper-demo.git", + "has_downloads": true, + "has_issues": true, + "has_pages": false, + "has_projects": true, + "has_wiki": true, + "homepage": "", + "hooks_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/hooks", + "html_url": "https://github.com/Matticusau/pr-helper-demo", + "id": 273344153, + "issue_comment_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/issues/events{/number}", + "issues_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/issues{/number}", + "keys_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/keys{/key_id}", + "labels_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/labels{/name}", + "language": null, + "languages_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/languages", + "license": { + "key": "mit", + "name": "MIT License", + "node_id": "MDc6TGljZW5zZTEz", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit" + }, + "merges_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/merges", + "milestones_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/milestones{/number}", + "mirror_url": null, + "name": "pr-helper-demo", + "node_id": "MDEwOlJlcG9zaXRvcnkyNzMzNDQxNTM=", + "notifications_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/notifications{?since,all,participating}", + "open_issues": 2, + "open_issues_count": 2, + "owner": { + "avatar_url": "https://avatars.githubusercontent.com/u/11083642?v=4", + "events_url": "https://api.github.com/users/Matticusau/events{/privacy}", + "followers_url": "https://api.github.com/users/Matticusau/followers", + "following_url": "https://api.github.com/users/Matticusau/following{/other_user}", + "gists_url": "https://api.github.com/users/Matticusau/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/Matticusau", + "id": 11083642, + "login": "Matticusau", + "node_id": "MDQ6VXNlcjExMDgzNjQy", + "organizations_url": "https://api.github.com/users/Matticusau/orgs", + "received_events_url": "https://api.github.com/users/Matticusau/received_events", + "repos_url": "https://api.github.com/users/Matticusau/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/Matticusau/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Matticusau/subscriptions", + "type": "User", + "url": "https://api.github.com/users/Matticusau" + }, + "private": true, + "pulls_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/pulls{/number}", + "pushed_at": "2021-02-07T12:04:23Z", + "releases_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/releases{/id}", + "size": 51, + "ssh_url": "git@github.com:Matticusau/pr-helper-demo.git", + "stargazers_count": 0, + "stargazers_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/stargazers", + "statuses_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/subscribers", + "subscription_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/subscription", + "svn_url": "https://github.com/Matticusau/pr-helper-demo", + "tags_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/tags", + "teams_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/teams", + "trees_url": "https://api.github.com/repos/Matticusau/pr-helper-demo/git/trees{/sha}", + "updated_at": "2021-02-07T12:04:25Z", + "url": "https://api.github.com/repos/Matticusau/pr-helper-demo", + "watchers": 0, + "watchers_count": 0 + }, + "sender": { + "avatar_url": "https://avatars.githubusercontent.com/u/11083642?v=4", + "events_url": "https://api.github.com/users/Matticusau/events{/privacy}", + "followers_url": "https://api.github.com/users/Matticusau/followers", + "following_url": "https://api.github.com/users/Matticusau/following{/other_user}", + "gists_url": "https://api.github.com/users/Matticusau/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/Matticusau", + "id": 11083642, + "login": "Matticusau", + "node_id": "MDQ6VXNlcjExMDgzNjQy", + "organizations_url": "https://api.github.com/users/Matticusau/orgs", + "received_events_url": "https://api.github.com/users/Matticusau/received_events", + "repos_url": "https://api.github.com/users/Matticusau/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/Matticusau/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Matticusau/subscriptions", + "type": "User", + "url": "https://api.github.com/users/Matticusau" + } + }, + "eventName": "project_card", + "sha": "0afe8d9b5854f2bf5e80ae60a8981c6785cb58e8", + "ref": "refs/heads/main", + "workflow": "ProjectCard Auto Labels", + "action": "Matticusauprojectcard-autolabel", + "actor": "Matticusau" +} +``` \ No newline at end of file diff --git a/lib/index.js b/lib/index.js index 39214aa..b6317fb 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1157,53 +1157,6 @@ function escapeProperty(s) { } //# sourceMappingURL=command.js.map -/***/ }), - -/***/ 168: -/***/ (function(__unusedmodule, exports) { - -"use strict"; - -// -// Author: Matt Lavery -// Date: 2021-02-07 -// Purpose: Handler for the Project Cards event -// -// When Who What -// ------------------------------------------------------------------------------------------ -// -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -// export default async function prHandler(core: CoreModule, context: Context, client: Client) { //, octokit: Client -function projectCardHandler(core, github) { - return __awaiter(this, void 0, void 0, function* () { - try { - core.info('context: ' + JSON.stringify(github.context)); - } - catch (error) { - // check for Not Found and soft exit, this might happen when an issue comment is detected - if (error.message === 'Not Found') { - core.info('prCommentHandler: Could not find PR. Might be triggered from an Issue.'); - return; - } - else { - core.setFailed(error.message); - throw error; - } - } - }); -} -exports.default = projectCardHandler; - - /***/ }), /***/ 183: @@ -1271,6 +1224,137 @@ function register(state, name, method, options) { module.exports = require("https"); +/***/ }), + +/***/ 225: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +// +// Author: Matt Lavery +// Date: 2021-02-07 +// Purpose: Handler for the Project Cards event +// +// When Who What +// ------------------------------------------------------------------------------------------ +// +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const classes_1 = __webpack_require__(525); +// export default async function prHandler(core: CoreModule, context: Context, client: Client) { //, octokit: Client +function projectCardMoveHandler(core, github) { + return __awaiter(this, void 0, void 0, function* () { + try { + //core.info('context: ' + JSON.stringify(github.context)); + const projectCardClass = new classes_1.ProjectCardClass(core, github); + // get the config + yield projectCardClass.GetConfig(); + var projectCardWebhookPayload = github.context.payload; + // make sure this is an issue and not a note + if (undefined !== projectCardWebhookPayload.project_card && undefined !== projectCardWebhookPayload.project_card.content_url) { + //core.info('content_url is defined'); + core.info('column_id: ' + projectCardWebhookPayload.project_card.column_id.toString()); + // octokit + const myToken = core.getInput('repo-token'); + const octokit = github.getOctokit(myToken); + // get the column details + const { data: columnResponseData } = yield octokit.projects.getColumn(Object.assign(Object.assign({}, github.context.repo), { column_id: projectCardWebhookPayload.project_card.column_id })); + //core.info('columnResponseData: ' + JSON.stringify(columnResponseData)); + core.info('column name: ' + columnResponseData.name); + // check this project should be processed + let projectNumber = projectCardClass.getProjectIdFromUrl(projectCardWebhookPayload.project_card.project_url); + if (yield projectCardClass.projectShouldBeProcessed(projectNumber)) { + // make sure the column matches one of our rules + if (yield projectCardClass.columnHasAutoLabelConfig(columnResponseData.name)) { + core.info('rule found for column'); + // get the issue number + let issueContentUrl = projectCardWebhookPayload.project_card.content_url; + // let issueNumber: number = 0; + // if (issueContentUrl.indexOf('/issues/') > 0) { + // issueContentUrl = issueContentUrl.substring(issueContentUrl.indexOf('/issues/') + 8) + // //core.info('issueContentUrl: ' + issueContentUrl); + // issueNumber = parseInt(issueContentUrl); + // } + let issueNumber = projectCardClass.getIssueNumberFromContentUrl(issueContentUrl); + // make sure we got the issue number + if (issueNumber > 0) { + // get the issue details + const { data: issueResponseData } = yield octokit.issues.get(Object.assign(Object.assign({}, github.context.repo), { issue_number: issueNumber })); + //core.info('issueResponseData: ' + JSON.stringify(issueResponseData)); + // get the issue labels + const { data: issueLabelsData } = yield octokit.issues.listLabelsOnIssue(Object.assign(Object.assign({}, github.context.repo), { issue_number: issueNumber })); + if (!issueLabelsData) { + core.error('Could not get pull request labels, exiting'); + return; + } + var issueLabels = new classes_1.IssueLabels(issueLabelsData); + //core.info('issueLabels: ' + JSON.stringify(issueLabels)); + // update the labels + //core.info('projectCardClass.labelsToRemove: ' + JSON.stringify(projectCardClass.labelsToRemove)); + if (undefined !== projectCardClass.labelsToRemove && projectCardClass.labelsToRemove.length > 0) { + // core.info('processing labels to remove'); + for (let iLabel = 0; iLabel < projectCardClass.labelsToRemove.length; iLabel++) { + // core.info('removing label: ' + projectCardClass.labelsToRemove[iLabel]); + issueLabels.removeLabel(projectCardClass.labelsToRemove[iLabel]); + } + } + //core.info('issueLabels: ' + JSON.stringify(issueLabels)); + //core.info('projectCardClass.labelsToAdd: ' + JSON.stringify(projectCardClass.labelsToAdd)); + if (undefined !== projectCardClass.labelsToAdd && projectCardClass.labelsToAdd.length > 0) { + // core.info('processing labels to add'); + for (let iLabel = 0; iLabel < projectCardClass.labelsToAdd.length; iLabel++) { + issueLabels.addLabel(projectCardClass.labelsToAdd[iLabel]); + } + } + if (issueLabels.haschanges) { + core.info('Updating labels on Issue ' + issueNumber.toString()); + // set the label + yield octokit.issues.setLabels({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + issue_number: issueNumber, + labels: issueLabels.labels + }); + } + } + else { + core.error('Could not determine Issue Number'); + } + } + else { + core.info('No rules found for column'); + } + } + else { + core.info('Project id ' + projectNumber + ' excluded by config'); + } + } + } + catch (error) { + // check for Not Found and soft exit, this might happen when an issue comment is detected + if (error.message === 'Not Found') { + core.info('prCommentHandler: Could not find PR. Might be triggered from an Issue.'); + return; + } + else { + core.setFailed(error.message); + throw error; + } + } + }); +} +exports.default = projectCardMoveHandler; + + /***/ }), /***/ 234: @@ -3102,6 +3186,301 @@ module.exports.array = (stream, options) => getStream(stream, Object.assign({}, module.exports.MaxBufferError = MaxBufferError; +/***/ }), + +/***/ 321: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PRHelper = void 0; +const index_1 = __webpack_require__(525); +class PRHelper { + constructor(core, github) { + this.core = core; + this.github = github; + this.mergemethod = "merge"; + } + setMergeMethod(method) { + if (method === 'squash') { + this.mergemethod = 'squash'; + } + else if (method === 'rebase') { + this.mergemethod = 'rebase'; + } + else { + this.mergemethod = 'merge'; + } + } + getPrNumber() { + var prNumber = undefined; + // event will determine how we get this from the payload + switch (this.github.context.eventName) { + case 'issue_comment': + const issue = this.github.context.payload.issue; + if (issue) { + prNumber = issue.number; + } + break; + default: + const pullRequest = this.github.context.payload.pull_request; + if (pullRequest) { + prNumber = pullRequest.number; + } + } + return prNumber; + } + getCommentNumber() { + var commentNumber = undefined; + // event will determine how we get this from the payload + switch (this.github.context.eventName) { + case 'issue_comment': + const comment = this.github.context.payload.comment; + if (comment) { + commentNumber = comment.id; + } + break; + default: + commentNumber = undefined; + } + return commentNumber; + } + isMergeReadyByState(pullRequest) { + try { + this.core.debug('>> isMergeReadyByState()'); + this.core.info('PR State: ' + pullRequest.state); + this.core.info('PR merged: ' + pullRequest.merged); + this.core.info('PR mergeable: ' + pullRequest.mergeable); + this.core.info('PR mergeable_state: ' + pullRequest.mergeable_state); + if (pullRequest.state !== 'closed') { + if (pullRequest.merged === false && pullRequest.mergeable === true) { + // is ready to merge + if (pullRequest.mergeable_state === 'clean' || pullRequest.mergeable_state === 'unstable') { + return true; + } + // if blocked check for pending reviewers (this doesn't factor in that the review is approved or not) + if (pullRequest.mergeable_state === 'blocked' && (pullRequest.requested_reviewers.length === 0 && pullRequest.requested_teams.length === 0)) { + this.core.info(`PR #${pullRequest.number} is blocked but has no outstanding reviews`); + return true; + } + // allow blocked state - if branch protection is enabled the blocked state will be returned by the GitHub Actions user + if (pullRequest.mergeable_state === 'blocked') { + return true; + } + } + } + return false; + } + catch (error) { + this.core.setFailed(error.message); + throw error; + } + } + isMergeReadyByLabel(pullRequest) { + return __awaiter(this, void 0, void 0, function* () { + try { + this.core.debug('>> isMergeReadyByLabel()'); + const myToken = this.core.getInput('repo-token'); + const octokit = this.github.getOctokit(myToken); + // check the labels + const { data: issueLabelsData } = yield octokit.issues.listLabelsOnIssue(Object.assign(Object.assign({}, this.github.context.repo), { issue_number: pullRequest.number })); + var issueLabels = new index_1.IssueLabels(issueLabelsData); + const qualifiesForAutoMerge = (issueLabels.hasLabelFromList([this.core.getInput('prlabel-automerge')])); + const readyToMergeLabel = (issueLabels.hasLabelFromList([this.core.getInput('prlabel-ready')])); + const NotReadyToMergeLabel = (issueLabels.hasLabelFromList([this.core.getInput('prlabel-reviewrequired'), this.core.getInput('prlabel-onhold')])); + this.core.info('qualifiesForAutoMerge: ' + qualifiesForAutoMerge); + this.core.info('readyToMergeLabel:' + readyToMergeLabel); + this.core.info('NotReadyToMergeLabel:' + NotReadyToMergeLabel); + if (qualifiesForAutoMerge && readyToMergeLabel && !NotReadyToMergeLabel) { + return true; + } + return false; + } + catch (error) { + this.core.setFailed(error.message); + throw error; + } + }); + } + isMergeReadyByReview(pullRequest) { + return __awaiter(this, void 0, void 0, function* () { + try { + this.core.debug('>> isMergeReadyByReview()'); + const myToken = this.core.getInput('repo-token'); + const octokit = this.github.getOctokit(myToken); + const requiredReviewCount = Number.parseInt(this.core.getInput('prmerge-requirereviewcount')); + // check the labels + const { data: reviewsData } = yield octokit.pulls.listReviews(Object.assign(Object.assign({}, this.github.context.repo), { pull_number: pullRequest.number })); + let reviews = { + total: reviewsData.length, + approved: 0, + request_changes: 0 + }; + let result = false; + // No outstanding reviews (this doesn't check for approval vs comment so only valid if requiredReviewCount disabled) + if (pullRequest.requested_reviewers.length === 0 && pullRequest.requested_teams.length === 0 && requiredReviewCount < 0) { + result = true; + } + // get the number of reviews + for (var iReview = 0; iReview < reviewsData.length; iReview++) { + if (reviewsData[iReview].state === 'APPROVED') { + reviews.approved++; + } + else if (reviewsData[iReview].state === 'REQUEST_CHANGES') { + reviews.request_changes++; + } + } + // check for reviews, and make sure no non-approved reviews + // if (reviews.total > 0 && (reviews.total === reviews.approved) && ((requiredReviewCount >= 0 && reviews.approved >= requiredReviewCount) || requiredReviewCount < 0)) { + if (reviews.total > 0 && (reviews.total === reviews.approved) && (requiredReviewCount >= 0 && reviews.approved >= requiredReviewCount)) { + this.core.info(`PR #${pullRequest.number} is mergable based on reviews`); + result = true; + } + // check for minimum number of required reviews (no requested changes) + if (requiredReviewCount >= 0 && reviews.approved >= requiredReviewCount && reviews.request_changes === 0) { + this.core.info(`PR #${pullRequest.number} is mergable based on minimum required reviews`); + result = true; + } + return result; + } + catch (error) { + this.core.setFailed(error.message); + throw error; + } + }); + } + isMergeReadyByChecks(pullRequest) { + return __awaiter(this, void 0, void 0, function* () { + try { + this.core.debug('>> isMergeReadyByChecks()'); + const requireallchecks = (this.core.getInput('prmerge-requireallchecks') === 'true'); + // not configured + if (requireallchecks !== true) { + this.core.info('require checks is not enabled'); + return true; + } + // need all checks + if (requireallchecks && (yield this.allChecksSucceeded(pullRequest))) { + this.core.info('required checks have all succeeded'); + return true; + } + // failed test + return false; + } + catch (error) { + this.core.setFailed(error.message); + throw error; + } + }); + } + allChecksSucceeded(pullRequest) { + return __awaiter(this, void 0, void 0, function* () { + try { + this.core.debug('>> allChecksPassed()'); + const myToken = this.core.getInput('repo-token'); + const octokit = this.github.getOctokit(myToken); + // check the labels + const { data: checksData } = yield octokit.checks.listForRef(Object.assign(Object.assign({}, this.github.context.repo), { ref: pullRequest.merge_commit_sha })); + let checks = { + total: checksData.total_count, + completed: 0, + success: 0 + }; + if (checksData && checksData.check_runs.length > 0) { + checksData.check_runs.forEach(element => { + if (element.status === "completed") { + checks.completed++; + } + if (element.conclusion === "success") { + checks.success++; + } + }); + } + return (checks.completed >= (checks.total - 1)) && (checks.success >= (checks.total - 1)); + } + catch (error) { + this.core.setFailed(error.message); + throw error; + } + }); + } + matchConfigFromActionInputYaml(json) { + try { + // convert json to string array + this.core.debug('json: ' + json); + //let pattern : string[] = JSON.parse(json); + let pattern = JSON.parse(json); + this.core.debug('json pattern: ' + JSON.stringify(pattern)); + return pattern; + } + catch (error) { + this.core.setFailed(error.message); + throw error; + } + } + isBranchDeleteReady(pullRequest) { + return __awaiter(this, void 0, void 0, function* () { + try { + this.core.debug('>> isBranchDeleteReady()'); + if (this.core.getInput('prmerge-deletebranch') === 'true') { + if (pullRequest.head.repo.id === pullRequest.base.repo.id) { + // not the default branch + if (pullRequest.base.repo.default_branch !== pullRequest.head.ref) { + const deleteBranchConfig = this.matchConfigFromActionInputYaml(this.core.getInput('prmerge-deletebranch-config')); + // check denies + if (deleteBranchConfig && deleteBranchConfig.deny && deleteBranchConfig.deny.length > 0) { + for (let iBranch = 0; iBranch < deleteBranchConfig.deny.length; iBranch++) { + // this.core.info(deleteBranchConfig.deny[iBranch]); + if (pullRequest.head.ref === deleteBranchConfig.deny[iBranch]) { + // we found a match so fail + return false; + } + } + } + // check allows + if (deleteBranchConfig && deleteBranchConfig.allow && deleteBranchConfig.allow.length > 0) { + for (let iBranch = 0; iBranch < deleteBranchConfig.allow.length; iBranch++) { + this.core.info(deleteBranchConfig.allow[iBranch]); + if (pullRequest.head.ref !== deleteBranchConfig.allow[iBranch]) { + // we found a non-match so fail + return false; + } + } + } + // check passed + return true; + } + else { + this.core.info('Base and Head are same branch, delete branch skipped.'); + } + } + else { + this.core.info('Base and Head not on same repo, delete branch skipped.'); + } + } + // failed test + return false; + } + catch (error) { + this.core.setFailed(error.message); + throw error; + } + }); + } +} +exports.PRHelper = PRHelper; + + /***/ }), /***/ 350: @@ -5160,7 +5539,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -const projectcardhandler_1 = __importDefault(__webpack_require__(168)); +const utils_1 = __webpack_require__(350); +const projectcardmovehandler_1 = __importDefault(__webpack_require__(225)); // import { ConfigHelper } from './classes'; // import prHello from './hello' function main(core, github) { @@ -5170,11 +5550,13 @@ function main(core, github) { // await config.loadConfig(core, github); // core.debug('config loaded'); // core.debug('config: ' + JSON.stringify(config.configuration)); - core.debug('context: ' + github.context); + core.debug('context: ' + JSON.stringify(github.context)); const event = github.context.eventName; switch (event) { - case 'project_cards': - yield projectcardhandler_1.default(core, github); + case 'project_card': + if (utils_1.context.payload.action == 'moved') { + yield projectcardmovehandler_1.default(core, github); + } } }); } @@ -5762,6 +6144,23 @@ module.exports = function (str) { }; +/***/ }), + +/***/ 525: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +// wrapper to import classes +var issuelabels_1 = __webpack_require__(705); +Object.defineProperty(exports, "IssueLabels", { enumerable: true, get: function () { return issuelabels_1.IssueLabels; } }); +var prhelper_1 = __webpack_require__(321); +Object.defineProperty(exports, "PRHelper", { enumerable: true, get: function () { return prhelper_1.PRHelper; } }); +var projectcard_1 = __webpack_require__(646); +Object.defineProperty(exports, "ProjectCardClass", { enumerable: true, get: function () { return projectcard_1.ProjectCardClass; } }); + + /***/ }), /***/ 532: @@ -6179,6 +6578,213 @@ exports.request = request; //# sourceMappingURL=index.js.map +/***/ }), + +/***/ 646: +/***/ (function(__unusedmodule, exports) { + +"use strict"; + +// +// Author: Matt Lavery +// Date: 2021-02-07 +// Purpose: Class to provide functionality to auto label based on card movement +// +// When Who What +// ------------------------------------------------------------------------------------------ +// +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ProjectCardClass = void 0; +// projectfilter-config: '{"include":["*"], "exclude":[]}' +// projectfilter-config: '{"include":[123456], "exclude":[645312]}' +class ProjectCardClass { + constructor(core, github) { + this.core = core; + this.github = github; + this.autoLabelConfig = []; + this.projectFilterConfig = {}; + this.labelsToAdd = []; + this.labelsToRemove = []; + } + matchConfigFromActionInputYaml(json) { + try { + // convert json to string array + this.core.debug('json: ' + json); + //let pattern : string[] = JSON.parse(json); + let pattern = JSON.parse(json); + this.core.debug('json pattern: ' + JSON.stringify(pattern)); + return pattern; + } + catch (error) { + this.core.setFailed(error.message); + throw error; + } + } + GetConfig() { + return __awaiter(this, void 0, void 0, function* () { + try { + yield this.GetProjectColumnAutoLabelConfig(); + yield this.GetProjectColumnFilterConfig(); + return true; + } + catch (error) { + this.core.setFailed(error.message); + throw error; + } + }); + } + GetProjectColumnAutoLabelConfig() { + return __awaiter(this, void 0, void 0, function* () { + try { + this.core.debug('>> GetProjectColumnAutoLabelConfig()'); + const autoLabelConfig = this.matchConfigFromActionInputYaml(this.core.getInput('autolabel-config')); + this.autoLabelConfig = autoLabelConfig; + return true; + } + catch (error) { + this.core.setFailed(error.message); + throw error; + } + }); + } + GetProjectColumnFilterConfig() { + return __awaiter(this, void 0, void 0, function* () { + try { + this.core.debug('>> GetProjectColumnFilterConfig()'); + let pattern = JSON.parse(this.core.getInput('projectfilter-config')); + this.core.debug('json pattern: ' + JSON.stringify(pattern)); + this.projectFilterConfig = pattern; + return true; + } + catch (error) { + this.core.setFailed(error.message); + throw error; + } + }); + } + columnHasAutoLabelConfig(columnName) { + return __awaiter(this, void 0, void 0, function* () { + try { + let blnResponse = false; + if (undefined !== this.autoLabelConfig && this.autoLabelConfig.length > 0) { + for (let iConfig = 0; iConfig < this.autoLabelConfig.length; iConfig++) { + if (this.autoLabelConfig[iConfig].column === columnName) { + blnResponse = true; + // while here set the label change variables + this.labelsToAdd = this.autoLabelConfig[iConfig].add_labels; + this.labelsToRemove = this.autoLabelConfig[iConfig].remove_labels; + break; + } + } + } + return blnResponse; + } + catch (error) { + this.core.setFailed(error.message); + throw error; + } + }); + } + // async GetLabelChangesToMake(columnName: string) : Promise { + // try { + // if (undefined !== this.autoLabelConfig && this.autoLabelConfig.length > 0) { + // for(let iConfig = 0; iConfig < this.autoLabelConfig.length; iConfig++) { + // if (this.autoLabelConfig[iConfig].column === columnName) { + // this.labelsToAdd = this.autoLabelConfig[iConfig].add_labels; + // this.labelsToRemove = this.autoLabelConfig[iConfig].remove_labels; + // break; + // } + // } + // } + // } catch (error) { + // this.core.setFailed(error.message); + // throw error; + // } + // } + getIssueNumberFromContentUrl(contentUrl) { + try { + let issueNumber = 0; + if (contentUrl.indexOf('/issues/') > 0) { + contentUrl = contentUrl.substring(contentUrl.indexOf('/issues/') + 8); + //core.info('issueContentUrl: ' + issueContentUrl); + issueNumber = parseInt(contentUrl); + } + return issueNumber; + } + catch (error) { + this.core.setFailed(error.message); + throw error; + } + } + getProjectIdFromUrl(projectUrl) { + try { + let projectNumber = 0; + if (projectUrl.indexOf('/issues/') > 0) { + projectUrl = projectUrl.substring(projectUrl.indexOf('/projects/') + 10); + //core.info('issueContentUrl: ' + issueContentUrl); + projectNumber = parseInt(projectUrl); + } + return projectNumber; + } + catch (error) { + this.core.setFailed(error.message); + throw error; + } + } + projectShouldBeProcessed(projectId) { + return __awaiter(this, void 0, void 0, function* () { + try { + let blnResponse = true; + // excluded overrides + if (undefined !== this.projectFilterConfig.exclude && this.projectFilterConfig.exclude.length > 0) { + for (let iExclude = 0; iExclude < this.projectFilterConfig.exclude.length; iExclude++) { + if (this.projectFilterConfig.exclude[iExclude].toString() === projectId.toString()) { + blnResponse = false; + break; + } + } + } + // now check inclusion + if (blnResponse) { + // flip the response now as we must match on include + blnResponse = false; + // if undefined then we assume included + if (undefined !== this.projectFilterConfig.include && this.projectFilterConfig.include.length > 0) { + // if we have a wild card + if (this.projectFilterConfig.include[0] === '*') { + blnResponse = true; + } + else { + for (let iInclude = 0; iInclude < this.projectFilterConfig.include.length; iInclude++) { + if (this.projectFilterConfig.include[iInclude].toString() === projectId.toString()) { + blnResponse = true; + break; + } + } + } + } + } + return blnResponse; + } + catch (error) { + this.core.setFailed(error.message); + throw error; + } + }); + } +} +exports.ProjectCardClass = ProjectCardClass; + + /***/ }), /***/ 667: @@ -6370,6 +6976,62 @@ exports.getUserAgent = getUserAgent; //# sourceMappingURL=index.js.map +/***/ }), + +/***/ 705: +/***/ (function(__unusedmodule, exports) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.IssueLabels = void 0; +// todo add content +class IssueLabels { + constructor(issuesListLabelsOnIssueResponseData) { + this.labels = this.getIssueLabelArray(issuesListLabelsOnIssueResponseData); + this.haschanges = false; + } + // add a label to the array in memory. Use octokit.issues.setLabels() to save + addLabel(label) { + if (label.length > 0) { + if (this.labels.indexOf(label) <= 0) { + this.labels.push(label); + this.haschanges = true; + } + } + } + // removes a label from the array in memory. Use octokit.issues.setLabels() to save + removeLabel(label) { + if (label.length > 0) { + if (this.labels.indexOf(label) >= 0) { + this.labels.splice(this.labels.indexOf(label), 1); + this.haschanges = true; + } + } + } + // checks if a label is assigned from a given list (i.e. checking allow/deny lists) + hasLabelFromList(allowDenyList) { + const matchedList = this.labels.filter((label) => allowDenyList.includes(label)); + if (matchedList.length > 0) { + return true; + } + else { + return false; + } + } + getIssueLabelArray(issueLabels) { + // var tmpArray : string[] = []; + // for (var i=0; i < issueLabels.length; i++) { + // tmpArray.push(issueLabels[i].name); + // } + // return tmpArray; + // convert to string array of names + return issueLabels.map((label) => label.name); + } +} +exports.IssueLabels = IssueLabels; + + /***/ }), /***/ 716: diff --git a/src/classes/issuelabels.ts b/src/classes/issuelabels.ts index 000d9d2..105d16d 100644 --- a/src/classes/issuelabels.ts +++ b/src/classes/issuelabels.ts @@ -26,8 +26,10 @@ export class IssueLabels { // add a label to the array in memory. Use octokit.issues.setLabels() to save addLabel(label : string) : void { if(label.length > 0) { - this.labels.push(label); - this.haschanges = true; + if (this.labels.indexOf(label) <= 0) { + this.labels.push(label); + this.haschanges = true; + } } } diff --git a/src/classes/projectcard.ts b/src/classes/projectcard.ts index 8a6b2fe..59aba8b 100644 --- a/src/classes/projectcard.ts +++ b/src/classes/projectcard.ts @@ -10,13 +10,201 @@ import { CoreModule, GitHubModule, Context, PullRequestPayload } from '../types'; import { IssueLabels } from './index'; import { PullsGetResponseData } from '@octokit/types/dist-types' +import { ProjectsGetCardResponseData } from '@octokit/types/dist-types' +interface AutoLabelConfig { + column: string; + add_labels?: string[]; + remove_labels?: string[]; +} +// autolabel-config: '[{"column":"In progress", "add_labels":["in-progress"], "remove_labels":["triage"]}]' -interface DeleteBranchConfig { - deny?: string[]; - allow?: string[]; +interface ProjectFilterConfig { + include?: string[]; + exclude?: string[]; } +// projectfilter-config: '{"include":["*"], "exclude":[]}' +// projectfilter-config: '{"include":[123456], "exclude":[645312]}' export class ProjectCardClass { + // properties + private core: CoreModule; + private github: GitHubModule; + private autoLabelConfig: AutoLabelConfig[]; + private projectFilterConfig: ProjectFilterConfig; + public labelsToAdd: string[] | undefined; + public labelsToRemove: string[] | undefined; + + constructor(core: CoreModule, github: GitHubModule) { + this.core = core; + this.github = github; + this.autoLabelConfig = []; + this.projectFilterConfig = {}; + this.labelsToAdd = []; + this.labelsToRemove = []; + } + + private matchConfigFromActionInputYaml(json: string) : AutoLabelConfig[] { + try{ + // convert json to string array + this.core.debug('json: ' + json); + //let pattern : string[] = JSON.parse(json); + let pattern : AutoLabelConfig[] = JSON.parse(json); + this.core.debug('json pattern: ' + JSON.stringify(pattern)); + + return pattern; + } catch (error) { + this.core.setFailed(error.message); + throw error; + } + } + + async GetConfig() : Promise { + try { + await this.GetProjectColumnAutoLabelConfig(); + await this.GetProjectColumnFilterConfig(); + return true; + } catch (error) { + this.core.setFailed(error.message); + throw error; + } + } + + + async GetProjectColumnAutoLabelConfig() : Promise { + try { + this.core.debug('>> GetProjectColumnAutoLabelConfig()'); + const autoLabelConfig : AutoLabelConfig[] = this.matchConfigFromActionInputYaml(this.core.getInput('autolabel-config')); + + this.autoLabelConfig = autoLabelConfig; + return true; + } catch (error) { + this.core.setFailed(error.message); + throw error; + } + } + + async GetProjectColumnFilterConfig() : Promise { + try { + this.core.debug('>> GetProjectColumnFilterConfig()'); + let pattern : ProjectFilterConfig = JSON.parse(this.core.getInput('projectfilter-config')); + this.core.debug('json pattern: ' + JSON.stringify(pattern)); + + this.projectFilterConfig = pattern; + return true; + } catch (error) { + this.core.setFailed(error.message); + throw error; + } + } + + async columnHasAutoLabelConfig(columnName: string) : Promise { + try { + let blnResponse: boolean = false; + if (undefined !== this.autoLabelConfig && this.autoLabelConfig.length > 0) { + for(let iConfig = 0; iConfig < this.autoLabelConfig.length; iConfig++) { + if (this.autoLabelConfig[iConfig].column === columnName) { + blnResponse = true; + + // while here set the label change variables + this.labelsToAdd = this.autoLabelConfig[iConfig].add_labels; + this.labelsToRemove = this.autoLabelConfig[iConfig].remove_labels; + + break; + } + } + } + return blnResponse; + } catch (error) { + this.core.setFailed(error.message); + throw error; + } + } + + // async GetLabelChangesToMake(columnName: string) : Promise { + // try { + // if (undefined !== this.autoLabelConfig && this.autoLabelConfig.length > 0) { + // for(let iConfig = 0; iConfig < this.autoLabelConfig.length; iConfig++) { + // if (this.autoLabelConfig[iConfig].column === columnName) { + // this.labelsToAdd = this.autoLabelConfig[iConfig].add_labels; + // this.labelsToRemove = this.autoLabelConfig[iConfig].remove_labels; + // break; + // } + // } + // } + // } catch (error) { + // this.core.setFailed(error.message); + // throw error; + // } + // } + + getIssueNumberFromContentUrl(contentUrl: string) : number { + try { + let issueNumber: number = 0; + if (contentUrl.indexOf('/issues/') > 0) { + contentUrl = contentUrl.substring(contentUrl.indexOf('/issues/') + 8) + //core.info('issueContentUrl: ' + issueContentUrl); + issueNumber = parseInt(contentUrl); + } + return issueNumber; + } catch (error) { + this.core.setFailed(error.message); + throw error; + } + } + + getProjectIdFromUrl(projectUrl: string) : number { + try { + let projectNumber: number = 0; + if (projectUrl.indexOf('/issues/') > 0) { + projectUrl = projectUrl.substring(projectUrl.indexOf('/projects/') + 10) + //core.info('issueContentUrl: ' + issueContentUrl); + projectNumber = parseInt(projectUrl); + } + return projectNumber; + } catch (error) { + this.core.setFailed(error.message); + throw error; + } + } + + async projectShouldBeProcessed(projectId: number) : Promise { + try { + let blnResponse: boolean = true; + // excluded overrides + if (undefined !== this.projectFilterConfig.exclude && this.projectFilterConfig.exclude.length > 0) { + for (let iExclude = 0; iExclude < this.projectFilterConfig.exclude.length; iExclude++ ) { + if (this.projectFilterConfig.exclude[iExclude].toString() === projectId.toString()) { + blnResponse = false; + break; + } + } + } + // now check inclusion + if (blnResponse) { + // flip the response now as we must match on include + blnResponse = false; + // if undefined then we assume included + if (undefined !== this.projectFilterConfig.include && this.projectFilterConfig.include.length > 0) { + // if we have a wild card + if (this.projectFilterConfig.include[0] === '*') { + blnResponse = true; + } else { + for (let iInclude = 0; iInclude < this.projectFilterConfig.include.length; iInclude++ ) { + if (this.projectFilterConfig.include[iInclude].toString() === projectId.toString()) { + blnResponse = true; + break; + } + } + } + + } + } + return blnResponse; + } catch (error) { + this.core.setFailed(error.message); + throw error; + } + } } diff --git a/src/main.ts b/src/main.ts index 4c3dea9..abbd59b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,7 +7,8 @@ // ------------------------------------------------------------------------------------------ // -import projectCardHandler from './projectcardhandler'; +import { context } from '@actions/github/lib/utils'; +import projectCardMoveHandler from './projectcardmovehandler'; import { CoreModule, GitHubModule } from './types'; // import { ConfigHelper } from './classes'; // import prHello from './hello' @@ -18,11 +19,13 @@ export default async function main(core: CoreModule, github: GitHubModule) { // await config.loadConfig(core, github); // core.debug('config loaded'); // core.debug('config: ' + JSON.stringify(config.configuration)); - core.debug('context: ' + github.context); + core.debug('context: ' + JSON.stringify(github.context)); const event = github.context.eventName switch (event) { - case 'project_cards': - await projectCardHandler(core, github); + case 'project_card': + if (context.payload.action == 'moved') { + await projectCardMoveHandler(core, github); + } } } diff --git a/src/projectcardhandler.ts b/src/projectcardhandler.ts deleted file mode 100644 index 038ef02..0000000 --- a/src/projectcardhandler.ts +++ /dev/null @@ -1,30 +0,0 @@ -// -// Author: Matt Lavery -// Date: 2021-02-07 -// Purpose: Handler for the Project Cards event -// -// When Who What -// ------------------------------------------------------------------------------------------ -// - -import { CoreModule, GitHubModule, Context } from './types'; // , Client -import { IssueLabels, ProjectCardClass } from './classes'; - -// export default async function prHandler(core: CoreModule, context: Context, client: Client) { //, octokit: Client -export default async function projectCardHandler(core: CoreModule, github: GitHubModule) { - - try { - core.info('context: ' + JSON.stringify(github.context)); - } - catch (error) { - // check for Not Found and soft exit, this might happen when an issue comment is detected - if (error.message === 'Not Found') { - core.info('prCommentHandler: Could not find PR. Might be triggered from an Issue.'); - return; - } else { - core.setFailed(error.message); - throw error; - } - } - -} diff --git a/src/projectcardmovehandler.ts b/src/projectcardmovehandler.ts new file mode 100644 index 0000000..44db152 --- /dev/null +++ b/src/projectcardmovehandler.ts @@ -0,0 +1,137 @@ +// +// Author: Matt Lavery +// Date: 2021-02-07 +// Purpose: Handler for the Project Cards event +// +// When Who What +// ------------------------------------------------------------------------------------------ +// + +import { CoreModule, GitHubModule, Context, ProjectCardWebhookPayload } from './types'; // , Client +import { IssueLabels, ProjectCardClass } from './classes'; +import { context } from '@actions/github/lib/utils'; + +// export default async function prHandler(core: CoreModule, context: Context, client: Client) { //, octokit: Client +export default async function projectCardMoveHandler(core: CoreModule, github: GitHubModule) { + + try { + //core.info('context: ' + JSON.stringify(github.context)); + + const projectCardClass = new ProjectCardClass(core, github); + + // get the config + await projectCardClass.GetConfig(); + + var projectCardWebhookPayload: ProjectCardWebhookPayload = github.context.payload; + // make sure this is an issue and not a note + if (undefined !== projectCardWebhookPayload.project_card && undefined !== projectCardWebhookPayload.project_card.content_url) { + //core.info('content_url is defined'); + core.info('column_id: ' + projectCardWebhookPayload.project_card.column_id.toString()); + + // octokit + const myToken = core.getInput('repo-token'); + const octokit = github.getOctokit(myToken); + + // get the column details + const { data: columnResponseData } = await octokit.projects.getColumn({ + ...github.context.repo, + column_id: projectCardWebhookPayload.project_card.column_id + }); + //core.info('columnResponseData: ' + JSON.stringify(columnResponseData)); + core.info('column name: ' + columnResponseData.name); + + // check this project should be processed + let projectNumber: number = projectCardClass.getProjectIdFromUrl(projectCardWebhookPayload.project_card.project_url); + if (await projectCardClass.projectShouldBeProcessed(projectNumber)) { + + // make sure the column matches one of our rules + if (await projectCardClass.columnHasAutoLabelConfig(columnResponseData.name)) { + core.info('rule found for column'); + + // get the issue number + let issueContentUrl: string = projectCardWebhookPayload.project_card.content_url; + // let issueNumber: number = 0; + // if (issueContentUrl.indexOf('/issues/') > 0) { + // issueContentUrl = issueContentUrl.substring(issueContentUrl.indexOf('/issues/') + 8) + // //core.info('issueContentUrl: ' + issueContentUrl); + // issueNumber = parseInt(issueContentUrl); + // } + let issueNumber: number = projectCardClass.getIssueNumberFromContentUrl(issueContentUrl); + + // make sure we got the issue number + if (issueNumber > 0) { + + // get the issue details + const { data: issueResponseData } = await octokit.issues.get({ + ...github.context.repo, + issue_number: issueNumber, + }); + //core.info('issueResponseData: ' + JSON.stringify(issueResponseData)); + + // get the issue labels + const { data: issueLabelsData } = await octokit.issues.listLabelsOnIssue({ + ...github.context.repo, + issue_number: issueNumber, + }); + + if (!issueLabelsData) { + core.error('Could not get pull request labels, exiting'); + return; + } + + var issueLabels = new IssueLabels(issueLabelsData); + //core.info('issueLabels: ' + JSON.stringify(issueLabels)); + + // update the labels + //core.info('projectCardClass.labelsToRemove: ' + JSON.stringify(projectCardClass.labelsToRemove)); + if (undefined !== projectCardClass.labelsToRemove && projectCardClass.labelsToRemove.length > 0) { + // core.info('processing labels to remove'); + for(let iLabel = 0; iLabel < projectCardClass.labelsToRemove.length; iLabel++) { + // core.info('removing label: ' + projectCardClass.labelsToRemove[iLabel]); + issueLabels.removeLabel(projectCardClass.labelsToRemove[iLabel]); + } + } + //core.info('issueLabels: ' + JSON.stringify(issueLabels)); + //core.info('projectCardClass.labelsToAdd: ' + JSON.stringify(projectCardClass.labelsToAdd)); + if (undefined !== projectCardClass.labelsToAdd && projectCardClass.labelsToAdd.length > 0) { + // core.info('processing labels to add'); + for(let iLabel = 0; iLabel < projectCardClass.labelsToAdd.length; iLabel++) { + issueLabels.addLabel(projectCardClass.labelsToAdd[iLabel]); + } + } + + if (issueLabels.haschanges) { + core.info('Updating labels on Issue ' + issueNumber.toString()); + // set the label + await octokit.issues.setLabels({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + issue_number: issueNumber, + labels: issueLabels.labels + }); + } + } else { + core.error('Could not determine Issue Number'); + } + } else { + core.info('No rules found for column'); + } + } else { + core.info('Project id ' + projectNumber + ' excluded by config'); + } + + } + + } + catch (error) { + // check for Not Found and soft exit, this might happen when an issue comment is detected + if (error.message === 'Not Found') { + core.info('prCommentHandler: Could not find PR. Might be triggered from an Issue.'); + return; + } else { + core.setFailed(error.message); + throw error; + } + } + +} diff --git a/src/types.ts b/src/types.ts index 30338f4..59ed018 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,6 +11,7 @@ import github from '@actions/github'; import core from '@actions/core'; +import Interfaces from '@actions/github/lib/interfaces' export type CoreModule = typeof core export type GitHubModule = typeof github @@ -73,4 +74,68 @@ export declare type PullRequestFilePayload = { contents_url: string; patch: string; // previous_filename: string; -}; \ No newline at end of file +}; + +export interface ProjectCardWebhookPayload { + [key: string]: any; + repository?: Interfaces.PayloadRepository; + issue?: { + [key: string]: any; + number: number; + html_url?: string; + body?: string; + }; + pull_request?: { + [key: string]: any; + number: number; + html_url?: string; + body?: string; + }; + sender?: { + [key: string]: any; + type: string; + }; + action?: string; + installation?: { + id: number; + [key: string]: any; + }; + comment?: { + id: number; + [key: string]: any; + }; + project_card?: { + after_id: number; + archived: boolean; + column_id: number; + column_url: string; + content_url: string; + created_at: string; + creator: { + avatar_url: string; + events_url: string; + followers_url: string; + following_url: string; + gists_url: string; + gravatar_id: string; + html_url: string; + id: number; + login: string; + node_id: string; + organizations_url: string; + received_events_url: string; + repos_url: string; + site_admin: false, + starred_url: string; + subscriptions_url: string; + type: string; + url: string; + }, + id: number; + node_id: string; + note: string; + project_url: string; + updated_at: string; + url: string; + } +} \ No newline at end of file