diff options
-rw-r--r-- | README.markdown | 4 | ||||
-rw-r--r-- | app/Action/Base.php | 1 | ||||
-rw-r--r-- | app/Action/CommentCreation.php | 83 | ||||
-rw-r--r-- | app/Controller/Project.php | 1 | ||||
-rw-r--r-- | app/Controller/Webhook.php | 4 | ||||
-rw-r--r-- | app/Locale/da_DK/translations.php | 5 | ||||
-rw-r--r-- | app/Locale/de_DE/translations.php | 5 | ||||
-rw-r--r-- | app/Locale/es_ES/translations.php | 5 | ||||
-rw-r--r-- | app/Locale/fi_FI/translations.php | 5 | ||||
-rw-r--r-- | app/Locale/fr_FR/translations.php | 5 | ||||
-rw-r--r-- | app/Locale/it_IT/translations.php | 5 | ||||
-rw-r--r-- | app/Locale/ja_JP/translations.php | 5 | ||||
-rw-r--r-- | app/Locale/pl_PL/translations.php | 5 | ||||
-rw-r--r-- | app/Locale/pt_BR/translations.php | 5 | ||||
-rw-r--r-- | app/Locale/ru_RU/translations.php | 5 | ||||
-rw-r--r-- | app/Locale/sv_SE/translations.php | 5 | ||||
-rw-r--r-- | app/Locale/th_TH/translations.php | 5 | ||||
-rw-r--r-- | app/Locale/zh_CN/translations.php | 5 | ||||
-rw-r--r-- | app/Model/Action.php | 2 | ||||
-rw-r--r-- | app/Model/GithubWebhook.php | 87 | ||||
-rw-r--r-- | app/Template/project_show.php | 11 | ||||
-rw-r--r-- | docs/automatic-actions.markdown | 11 | ||||
-rw-r--r-- | docs/github-webhooks.markdown | 94 |
23 files changed, 348 insertions, 15 deletions
diff --git a/README.markdown b/README.markdown index e235eb80..c8650a9c 100644 --- a/README.markdown +++ b/README.markdown @@ -98,6 +98,10 @@ Documentation - [GitHub authentication](docs/github-authentication.markdown) - [Reverse proxy authentication](docs/reverse-proxy-authentication.markdown) +### Integration + +- [Github webhooks](docs/github-webhooks.markdown) + #### Developers and sysadmins - [Board configuration](docs/board-configuration.markdown) diff --git a/app/Action/Base.php b/app/Action/Base.php index 80930a4c..be9c3d48 100644 --- a/app/Action/Base.php +++ b/app/Action/Base.php @@ -13,6 +13,7 @@ use Core\Tool; * @author Frederic Guillot * * @property \Model\Acl $acl + * @property \Model\Comment $comment * @property \Model\Task $task * @property \Model\TaskFinder $taskFinder */ diff --git a/app/Action/CommentCreation.php b/app/Action/CommentCreation.php new file mode 100644 index 00000000..3543403d --- /dev/null +++ b/app/Action/CommentCreation.php @@ -0,0 +1,83 @@ +<?php + +namespace Action; + +use Model\GithubWebhook; + +/** + * Create automatically a comment from a webhook + * + * @package action + * @author Frederic Guillot + */ +class CommentCreation extends Base +{ + /** + * Get the list of compatible events + * + * @access public + * @return array + */ + public function getCompatibleEvents() + { + return array( + GithubWebhook::EVENT_ISSUE_COMMENT, + ); + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array(); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return array + */ + public function getEventRequiredParameters() + { + return array( + 'reference', + 'comment', + 'user_id', + 'task_id', + ); + } + + /** + * Execute the action (create a new comment) + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + return $this->comment->create(array( + 'reference' => $data['reference'], + 'comment' => $data['comment'], + 'task_id' => $data['task_id'], + 'user_id' => $data['user_id'], + )); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return true; + } +} diff --git a/app/Controller/Project.php b/app/Controller/Project.php index d749ef53..c5f16496 100644 --- a/app/Controller/Project.php +++ b/app/Controller/Project.php @@ -54,6 +54,7 @@ class Project extends Base $this->response->html($this->projectLayout('project_show', array( 'project' => $project, 'stats' => $this->project->getStats($project['id']), + 'webhook_token' => $this->config->get('webhook_token'), 'title' => $project['name'], ))); } diff --git a/app/Controller/Webhook.php b/app/Controller/Webhook.php index 71acab08..fa9c5834 100644 --- a/app/Controller/Webhook.php +++ b/app/Controller/Webhook.php @@ -55,9 +55,11 @@ class Webhook extends Base $this->githubWebhook->setProjectId($this->request->getIntegerParam('project_id')); - $this->githubWebhook->parsePayload( + $result = $this->githubWebhook->parsePayload( $this->request->getHeader('X-Github-Event'), $this->request->getBody() ); + + echo $result ? 'PARSED' : 'IGNORED'; } } diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index f3523aa4..5c8fb07f 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -553,4 +553,9 @@ return array( // 'Everybody have access to this project.' => '', // 'Webhooks' => '', // 'API' => '', + // 'Integration' => '', + // 'Github webhook' => '', + // 'Help on Github webhook' => '', + // 'Create a comment from an external provider' => '', + // 'Github issue comment created' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index f9124641..eb6c8607 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -553,4 +553,9 @@ return array( // 'Everybody have access to this project.' => '', // 'Webhooks' => '', // 'API' => '', + // 'Integration' => '', + // 'Github webhook' => '', + // 'Help on Github webhook' => '', + // 'Create a comment from an external provider' => '', + // 'Github issue comment created' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index a18597fc..479983e7 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -553,4 +553,9 @@ return array( // 'Everybody have access to this project.' => '', // 'Webhooks' => '', // 'API' => '', + // 'Integration' => '', + // 'Github webhook' => '', + // 'Help on Github webhook' => '', + // 'Create a comment from an external provider' => '', + // 'Github issue comment created' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index e5303adc..78a7c84e 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -553,4 +553,9 @@ return array( // 'Everybody have access to this project.' => '', // 'Webhooks' => '', // 'API' => '', + // 'Integration' => '', + // 'Github webhook' => '', + // 'Help on Github webhook' => '', + // 'Create a comment from an external provider' => '', + // 'Github issue comment created' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index 8b1287ce..5f7e7935 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -553,4 +553,9 @@ return array( 'Everybody have access to this project.' => 'Tout le monde a acccès à ce projet.', 'Webhooks' => 'Webhooks', 'API' => 'API', + 'Integration' => 'Intégration', + 'Github webhook' => 'Webhook Github', + 'Help on Github webhook' => 'Aide sur les webhooks Github', + 'Create a comment from an external provider' => 'Créer un commentaire depuis un fournisseur externe', + 'Github issue comment created' => 'Commentaire créé sur un ticket Github', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index 2ec81f21..e0b2b895 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -553,4 +553,9 @@ return array( // 'Everybody have access to this project.' => '', // 'Webhooks' => '', // 'API' => '', + // 'Integration' => '', + // 'Github webhook' => '', + // 'Help on Github webhook' => '', + // 'Create a comment from an external provider' => '', + // 'Github issue comment created' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index e05c13e3..440c1e8a 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -553,4 +553,9 @@ return array( // 'Everybody have access to this project.' => '', // 'Webhooks' => '', // 'API' => '', + // 'Integration' => '', + // 'Github webhook' => '', + // 'Help on Github webhook' => '', + // 'Create a comment from an external provider' => '', + // 'Github issue comment created' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index b3c6acf1..a294de75 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -553,4 +553,9 @@ return array( // 'Everybody have access to this project.' => '', // 'Webhooks' => '', // 'API' => '', + // 'Integration' => '', + // 'Github webhook' => '', + // 'Help on Github webhook' => '', + // 'Create a comment from an external provider' => '', + // 'Github issue comment created' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index 902943cc..6986e785 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -553,4 +553,9 @@ return array( // 'Everybody have access to this project.' => '', // 'Webhooks' => '', // 'API' => '', + // 'Integration' => '', + // 'Github webhook' => '', + // 'Help on Github webhook' => '', + // 'Create a comment from an external provider' => '', + // 'Github issue comment created' => '', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index a1c79de3..ae752520 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -553,4 +553,9 @@ return array( // 'Everybody have access to this project.' => '', // 'Webhooks' => '', // 'API' => '', + // 'Integration' => '', + // 'Github webhook' => '', + // 'Help on Github webhook' => '', + // 'Create a comment from an external provider' => '', + // 'Github issue comment created' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index dbd21401..d3a1c5ed 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -553,4 +553,9 @@ return array( // 'Everybody have access to this project.' => '', // 'Webhooks' => '', // 'API' => '', + // 'Integration' => '', + // 'Github webhook' => '', + // 'Help on Github webhook' => '', + // 'Create a comment from an external provider' => '', + // 'Github issue comment created' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index 6c6a41bf..65ea09ba 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -553,4 +553,9 @@ return array( 'Everybody have access to this project.' => 'ทุกคนสามารถเข้าถึงโปรเจคนี้', // 'Webhooks' => '', // 'API' => '', + // 'Integration' => '', + // 'Github webhook' => '', + // 'Help on Github webhook' => '', + // 'Create a comment from an external provider' => '', + // 'Github issue comment created' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index bfe15067..11f45f7d 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -553,4 +553,9 @@ return array( // 'Everybody have access to this project.' => '', // 'Webhooks' => '', // 'API' => '', + // 'Integration' => '', + // 'Github webhook' => '', + // 'Help on Github webhook' => '', + // 'Create a comment from an external provider' => '', + // 'Github issue comment created' => '', ); diff --git a/app/Model/Action.php b/app/Model/Action.php index 56a1a2bb..c3acdc5b 100644 --- a/app/Model/Action.php +++ b/app/Model/Action.php @@ -46,6 +46,7 @@ class Action extends Base 'TaskAssignColorUser' => t('Assign a color to a specific user'), 'TaskAssignColorCategory' => t('Assign automatically a color based on a category'), 'TaskAssignCategoryColor' => t('Assign automatically a category based on a color'), + 'CommentCreation' => t('Create a comment from an external provider'), 'TaskCreation' => t('Create a task from an external provider'), 'TaskAssignUser' => t('Change the assignee based on an external username'), 'TaskAssignCategoryLabel' => t('Change the category based on an external label'), @@ -78,6 +79,7 @@ class Action extends Base GithubWebhook::EVENT_ISSUE_REOPENED => t('Github issue reopened'), GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE => t('Github issue assignee change'), GithubWebhook::EVENT_ISSUE_LABEL_CHANGE => t('Github issue label change'), + GithubWebhook::EVENT_ISSUE_COMMENT => t('Github issue comment created'), ); asort($values); diff --git a/app/Model/GithubWebhook.php b/app/Model/GithubWebhook.php index 6624a782..58c2fa54 100644 --- a/app/Model/GithubWebhook.php +++ b/app/Model/GithubWebhook.php @@ -48,6 +48,7 @@ class GithubWebhook extends Base * @access public * @param string $type Github event type * @param string $payload Raw Github event (JSON) + * @return boolean */ public function parsePayload($type, $payload) { @@ -58,7 +59,11 @@ class GithubWebhook extends Base return $this->parsePushEvent($payload); case 'issues': return $this->parseIssueEvent($payload); + case 'issue_comment': + return $this->parseCommentIssueEvent($payload); } + + return false; } /** @@ -66,6 +71,7 @@ class GithubWebhook extends Base * * @access public * @param array $payload Event data + * @return boolean */ public function parsePushEvent(array $payload) { @@ -87,6 +93,8 @@ class GithubWebhook extends Base $this->event->trigger(self::EVENT_COMMIT, array('task_id' => $task_id) + $task); } } + + return true; } /** @@ -94,32 +102,57 @@ class GithubWebhook extends Base * * @access public * @param array $payload Event data + * @return boolean */ public function parseIssueEvent(array $payload) { switch ($payload['action']) { case 'opened': - $this->handleIssueOpened($payload['issue']); - break; + return $this->handleIssueOpened($payload['issue']); case 'closed': - $this->handleIssueClosed($payload['issue']); - break; + return $this->handleIssueClosed($payload['issue']); case 'reopened': - $this->handleIssueReopened($payload['issue']); - break; + return $this->handleIssueReopened($payload['issue']); case 'assigned': - $this->handleIssueAssigned($payload['issue']); - break; + return $this->handleIssueAssigned($payload['issue']); case 'unassigned': - $this->handleIssueUnassigned($payload['issue']); - break; + return $this->handleIssueUnassigned($payload['issue']); case 'labeled': - $this->handleIssueLabeled($payload['issue'], $payload['label']); - break; + return $this->handleIssueLabeled($payload['issue'], $payload['label']); case 'unlabeled': - $this->handleIssueUnlabeled($payload['issue'], $payload['label']); - break; + return $this->handleIssueUnlabeled($payload['issue'], $payload['label']); + } + + return false; + } + + /** + * Parse comment issue events + * + * @access public + * @param array $payload Event data + * @return boolean + */ + public function parseCommentIssueEvent(array $payload) + { + $task = $this->taskFinder->getByReference($payload['issue']['number']); + $user = $this->user->getByUsername($payload['comment']['user']['login']); + + if ($task && $user) { + + $event = array( + 'project_id' => $this->project_id, + 'reference' => $payload['comment']['id'], + 'comment' => $payload['comment']['body'], + 'user_id' => $user['id'], + 'task_id' => $task['id'], + ); + + $this->event->trigger(self::EVENT_ISSUE_COMMENT, $event); + return true; } + + return false; } /** @@ -127,6 +160,7 @@ class GithubWebhook extends Base * * @access public * @param array $issue Issue data + * @return boolean */ public function handleIssueOpened(array $issue) { @@ -138,6 +172,7 @@ class GithubWebhook extends Base ); $this->event->trigger(self::EVENT_ISSUE_OPENED, $event); + return true; } /** @@ -145,6 +180,7 @@ class GithubWebhook extends Base * * @access public * @param array $issue Issue data + * @return boolean */ public function handleIssueClosed(array $issue) { @@ -158,7 +194,10 @@ class GithubWebhook extends Base ); $this->event->trigger(self::EVENT_ISSUE_CLOSED, $event); + return true; } + + return false; } /** @@ -166,6 +205,7 @@ class GithubWebhook extends Base * * @access public * @param array $issue Issue data + * @return boolean */ public function handleIssueReopened(array $issue) { @@ -179,7 +219,10 @@ class GithubWebhook extends Base ); $this->event->trigger(self::EVENT_ISSUE_REOPENED, $event); + return true; } + + return false; } /** @@ -187,6 +230,7 @@ class GithubWebhook extends Base * * @access public * @param array $issue Issue data + * @return boolean */ public function handleIssueAssigned(array $issue) { @@ -203,7 +247,10 @@ class GithubWebhook extends Base ); $this->event->trigger(self::EVENT_ISSUE_ASSIGNEE_CHANGE, $event); + return true; } + + return false; } /** @@ -211,6 +258,7 @@ class GithubWebhook extends Base * * @access public * @param array $issue Issue data + * @return boolean */ public function handleIssueUnassigned(array $issue) { @@ -226,7 +274,10 @@ class GithubWebhook extends Base ); $this->event->trigger(self::EVENT_ISSUE_ASSIGNEE_CHANGE, $event); + return true; } + + return false; } /** @@ -235,6 +286,7 @@ class GithubWebhook extends Base * @access public * @param array $issue Issue data * @param array $label Label data + * @return boolean */ public function handleIssueLabeled(array $issue, array $label) { @@ -250,7 +302,10 @@ class GithubWebhook extends Base ); $this->event->trigger(self::EVENT_ISSUE_LABEL_CHANGE, $event); + return true; } + + return false; } /** @@ -259,6 +314,7 @@ class GithubWebhook extends Base * @access public * @param array $issue Issue data * @param array $label Label data + * @return boolean */ public function handleIssueUnlabeled(array $issue, array $label) { @@ -275,6 +331,9 @@ class GithubWebhook extends Base ); $this->event->trigger(self::EVENT_ISSUE_LABEL_CHANGE, $event); + return true; } + + return false; } } diff --git a/app/Template/project_show.php b/app/Template/project_show.php index facdc60a..907d1b70 100644 --- a/app/Template/project_show.php +++ b/app/Template/project_show.php @@ -53,3 +53,14 @@ </tr> <?php endforeach ?> </table> + +<?php if (Helper\is_admin()): ?> +<div class="page-header"> + <h2><?= t('Integration') ?></h2> +</div> + +<h3><i class="fa fa-github fa-fw"></i><?= t('Github webhook') ?></h3> +<input type="text" readonly="readonly" value="<?= Helper\get_current_base_url().Helper\u('webhook', 'github', array('token' => $webhook_token, 'project_id' => $project['id'])) ?>"/><br/> +<p class="form-help"><a href="http://kanboard.net/documentation/github-webhooks" target="_blank"><?= t('Help on Github webhook') ?></a></p> + +<?php endif ?> diff --git a/docs/automatic-actions.markdown b/docs/automatic-actions.markdown index 631919ea..cf350a2f 100644 --- a/docs/automatic-actions.markdown +++ b/docs/automatic-actions.markdown @@ -32,6 +32,13 @@ List of available events - Closing a task - Task creation or modification - Task assignee change +- Github commit received +- Github issue opened +- Github issue closed +- Github issue reopened +- Github issue assignee change +- Github issue label change +- Github issue comment created List of available actions ------------------------- @@ -44,6 +51,10 @@ List of available actions - Assign a color to a specific user - Assign automatically a color based on a category - Assign automatically a category based on a color +- Create a task from an external provider +- Change the assignee based on an external username +- Change the category based on an external label +- Create a comment from an external provider Examples -------- diff --git a/docs/github-webhooks.markdown b/docs/github-webhooks.markdown new file mode 100644 index 00000000..cdbb69c9 --- /dev/null +++ b/docs/github-webhooks.markdown @@ -0,0 +1,94 @@ +Github webhook integration +========================== + +Kanboard can be synchronized with Github. +Currently, it's only a one-way synchronization: Github to Kanboard. + +Github webhooks are plugged to Kanboard automatic actions. +When an event occurs on Github, an action can be performed on Kanboard. + +List of available events +------------------------ + +- Github commit received +- Github issue opened +- Github issue closed +- Github issue reopened +- Github issue assignee change +- Github issue label change +- Github issue comment created + +List of available actions +------------------------- + +- Create a task from an external provider +- Change the assignee based on an external username +- Change the category based on an external label +- Create a comment from an external provider + +Configuration on Github +----------------------- + +Go to your project settings page, on the left choose "Webhooks & Services", then click on the button "Add webhook". + +![Github configuration](http://kanboard.net/screenshots/documentation/github-webhooks.png) + +- **Payload url**: This url is available on Kanboard, go to "projects", select your project, at the bottom of the page, you have a section Github webhooks, just copy and paste the link. +- Select **"Send me everything"** + +Each time an event happens, Github will send an event to Kanboard now. +The Kanboard webhook url is protected by a random token. + +Everything else is handled by automatic actions in your Kanboard project settings. + +Examples +-------- + +### Close a Kanboard task when a commit pushed to Github + +- Choose the event: **Github commit received** +- Choose the action: **Close the task** + +When one or more commits are sent to Github, Kanboard will receive the information, each commit message with a task number included will be closed. + +Example: + +- Commit message: "Fix bug #1234" +- That will close the Kanboard task #1234 + +### Create a Kanboard task when a new issue is opened on Github + +- Choose the event: **Github issue opened** +- Choose the action: **Create a task from an external provider** + +When a task is created from a Github issue, the link to the issue is added to the description and the task have a new field named "Reference" (this is the Github ticket number). + +### Close a Kanboard task when an issue is closed on Github + +- Choose the event: **Github issue closed** +- Choose the action: **Close the task** + +### Reopen a Kanboard task when an issue is reopened on Github + +- Choose the event: **Github issue reopened** +- Choose the action: **Open the task** + +### Assign a task to a Kanboard user when an issue is assigned on Github + +- Choose the event: **Github issue assignee change** +- Choose the action: **Change the assignee based on an external username** + +Note: The username must be the same between Github and Kanboard. + +### Assign a category when an issue is tagged on Github + +- Choose the event: **Github issue label change** +- Choose the action: **Change the category based on an external label** +- Define the label and the category + +### Create a comment on Kanboard when an issue is commented on Github + +- Choose the event: **Github issue comment created** +- Choose the action: **Create a comment from an external provider** + +Note: The username of the comment author must be the same between Github and Kanboard and the task must exists before. |