diff --git a/build.gradle b/build.gradle
index fbd6a6a..6babed9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -15,8 +15,8 @@ dependencies {
implementation 'org.kohsuke:github-api:1.95'
implementation 'com.electriccloud.plugins:flowpdf-groovy-lib:1.1.1.0'
- //That's ours
- //implementation 'com.electriccloud:commander-api-bindings:9.0.0-SNAPSHOT'
+ //That's ours (you can uncomment this for local testing)
+// implementation 'com.electriccloud:commander-api-bindings:9.0.0-SNAPSHOT'
}
diff --git a/config/pluginspec.yaml b/config/pluginspec.yaml
index 68ac6f9..e6ecd4f 100644
--- a/config/pluginspec.yaml
+++ b/config/pluginspec.yaml
@@ -1,6 +1,6 @@
pluginInfo:
pluginName: EC-Github
- version: 3.3.0
+ version: 3.4.0
description: CloudBees CD integration for Github API
author: Polina
authorUrl: pshubina@cloudbees.com
@@ -27,18 +27,16 @@ properties:
- propertyName: ec_webhook
credentialProtected: true
properties:
- - propertyName: displayName
- value: GitHub
- - propertyName: procedureName
- value: webhook
- - propertyName: script
- value:
- path: dsl/properties/ec_webhook/script.groovy
- - propertyName: setupProcedure
- value: SetupWebhook
- - propertyName: ec_polling
- properties:
- displayName: GitHub
+ - propertyName: default
+ properties:
+ - propertyName: displayName
+ value: GitHub
+ - propertyName: procedureName
+ value: webhook
+ - propertyName: script
+ value: '$[/myProject/ec_webhook/script.groovy]'
+ - propertyName: setupProcedure
+ value: SetupWebhook
procedures:
- name: SetupWebhook
@@ -63,28 +61,77 @@ procedures:
# this is a webhook backing procedure (gives form for the webhook)
parameters:
- name: repositories
+ label: Repositories
+ documentation: List of repositories, separated by a newline. Leave empty to process events for all the repositories where the webhook is set up.
type: textarea
- documentation: List of repositories, separated by a newline.
required: false
- name: pushEvent
+ label: Process Push Events?
type: checkbox
- label: Process Push?
- documentation: Processes push events
- - name: prEvent
- type: checkbox
- label: Process Pull Request?
- - name: prAction
- type: textarea
- dependsOn: prEvent
- condition: ${prEvent == "true"}
- documentation: The action that was performed. Can be one of opened, edited, closed, assigned, unassigned, review_requested, review_request_removed, ready_for_review, labeled, unlabeled, synchronize, locked, unlocked, or reopened. If the action is closed and the merged key is false, the pull request was closed with unmerged commits. If the action is closed and the merged key is true, the pull request was merged.
+ documentation: Check this if you want trigger to be run when the new commit appears in one of the monitored branches.
- name: includeBranches
+ label: Include Branches
+ documentation: |
+ List of branch names, separated by a comma.
+ Incoming events will be discarded if not relate to one of the specified branches.
+ Leave empty to process events for all branches except ones specified in the “excludeBranches” parameter.
type: textarea
dependsOn: pushEvent
- condition: ${pushEvent == "true"}
+ condition: ${pushEvent} == "true"
- name: excludeBranches
+ label: Exclude Branches
type: textarea
+ documentation: |
+ List of branch names, separated by a comma.
+ Incoming events will be discarded if relate to one of the specified branches.
+ Leave empty to process events for all branches or only for the specified in the "Include Branches".
required: false
+ dependsOn: pushEvent
+ condition: ${pushEvent} == "true"
+ - name: prEvent
+ label: Process Pull Requests?
+ documentation: |
+ Check this if you want the trigger to be run when Pull Request event occurs
+ type: checkbox
+ required: false
+ - name: includePrActions
+ type: textarea
+ dependsOn: prEvent
+ condition: ${prEvent} == "true"
+ documentation: |
+ The action that was performed. Can be one of:
+
- opened
+ - edited
+ - closed_merged
+ - closed_discarded
+ - assigned
+ - unassigned
+ - review_requested
+ - review_request_removed
+ - ready_for_review
+ - labeled
+ - unlabeled
+ - synchronize
+ - locked
-
+
- unlocked
+ - reopened
+
+ - name: commitStatusEvent
+ label: Process Commit Status Events?
+ type: checkbox
+ documentation: Check this if you want trigger to be run when a commit status has been changed.
+ required: false
+ - name: includeCommitStatuses
+ label: Include Commit Status Events
+ type: textarea
+ documentation: |
+ Limit to following commit statuses. Comma-separated list with following statuses:
+
+ - pending
+ - success
+ - failure
+ - error
+
- name: Create Repository
description: Creates a GitHub Repository
diff --git a/dsl/properties/ec_webhook/script.groovy b/dsl/properties/ec_webhook/script.groovy
index cfe91a2..abadedf 100644
--- a/dsl/properties/ec_webhook/script.groovy
+++ b/dsl/properties/ec_webhook/script.groovy
@@ -1,50 +1,450 @@
+import groovy.json.JsonSlurper
+import org.apache.commons.codec.binary.Hex
+
+import javax.crypto.Mac
+import javax.crypto.spec.SecretKeySpec
+import java.util.regex.Pattern
+
+import static java.nio.charset.StandardCharsets.UTF_8
+
println args
def trigger = args.trigger
-def headers = args.headers
-def method = args.method
-def body = args.body
-def url = args.url
+Map headers = args.headers
+String method = args.method
+String body = args.body
+String url = args.url
def query = args.query
-// do something
+// This map corresponds to the procedure form
+final Map SUPPORTED_EVENTS = [
+ 'push' : [
+ enabledParamName: 'pushEvent',
+ actionsParamName: null
+ ],
+ 'pull_request': [
+ enabledParamName: 'prEvent',
+ actionsParamName: 'includePrActions'
+ ],
+ 'status' : [
+ enabledParamName: 'commitStatusEvent',
+ actionsParamName: 'includeCommitStatuses'
+ ],
+ 'ping' : [
+ enabledParamName: null,
+ actionsParamName: null
+ ],
+]
-def event = ''
-def signature = ''
+// Parsing headers
+String event = headers.get('X-GitHub-Event')
+String signature = headers.get('X-Hub-Signature')
+if (!signature) {
+ throw new RuntimeException("Request does not contain the signature header")
+}
+if (!event) {
+ throw new RuntimeException("Request does not contain the event header")
+}
-headers.each { k, v ->
- if (k.toLowerCase() == 'X-GitHub-Event') {
- event = v
- }
- if (k.toLowerCase() == 'X-Hub-Signature') {
- signature = v
- }
+// As we do not use other restrictions, every trigger should have a signature secret
+if (!trigger.webhookSecret) {
+ throw new RuntimeException("Trigger '${trigger.getName()}' does not have webhookSecret set up")
+}
+//validating signature
+if (!verifySignedPayload(signature, (String) trigger.webhookSecret, body)) {
+ // Todo: change to agreed exception
+ throw new RuntimeException("Signatures do not match. Please check that the trigger's 'webhookSecret' field value matches one specified in the Github repository webhook settings.")
}
-//validate signature
+// Receiving trigger parameters
+def paramsPropertySheet = trigger.pluginParameters
+Map pluginParameters = [:]
+paramsPropertySheet['properties'].each { String k, Map v ->
+ pluginParameters[k] = v['value']
+}
+
+
+WebhookEvent webhookEvent = WebhookEvent.getForType(event, body)
+if (webhookEvent == null) {
+ return [
+ responseMessage: "Ignoring unsupported '${event}' event",
+ launchWebhook : false
+ ]
+}
-if (event == 'ping') {
+// Check repository
+String repositoryName = webhookEvent.getRepositoryName()
+if (!repositoryName) {
+ throw new RuntimeException("Webhook event '${event}' doesn't contain 'repository' object")
+}
+if (!doCheckRepositoryIncluded(pluginParameters.get('repositories'), repositoryName)) {
return [
- eventType : 'ping',
- webhookData : ['some data': 'some data'],
- commitId : null,
- commitAuthorName : null,
- commitAuthorEmail: null,
- branch : null,
- launchWebhook : false
+ responseMessage: "Ignoring ${repositoryName} repository event",
+ launchWebhook : false
]
-} else if (event == 'push') {
- def payload = new JsonSlurper().parseText(body)
- def commits = payload.commits
- def repo = payload.repository
-}
-
-return [
- eventType : 'push',
- webhookData : ['some data': 'some data'],
- commitId : null,
- commitAuthorName : null,
- commitAuthorEmail: null,
- branch : null,
- launchWebhook : true
-]
\ No newline at end of file
+}
+
+// Check event enabled
+String eventEnabledParamName = SUPPORTED_EVENTS[event]['enabledParamName']
+if (eventEnabledParamName != null) {
+
+ boolean eventEnabled = pluginParameters.get(eventEnabledParamName) != 'false'
+ if (!eventEnabled) {
+ return [
+ responseMessage: "Processing for the '${webhookEvent.getName()}' event is disabled",
+ launchWebhook : false
+ ]
+ }
+}
+
+
+// Check action enabled
+String includedActionParamName = SUPPORTED_EVENTS[event]['actionsParamName']
+if (includedActionParamName != null) {
+
+ String includedActions = pluginParameters.get(includedActionParamName)
+ String eventAction = webhookEvent.getAction()
+
+ boolean actionEnabled = doCheckActionIncluded(includedActions, eventAction)
+
+ if (!actionEnabled) {
+ return [
+ responseMessage: "Processing for the '${eventAction}' action of the '${event}' event is disabled",
+ launchWebhook : false
+ ]
+ }
+}
+
+// Check that branch is included and not excluded
+String includeBranches = pluginParameters.get('includeBranches')
+String excludeBranches = pluginParameters.get('excludeBranches')
+
+ArrayList eventBranches = webhookEvent.getBranchNames()
+String branchName = eventBranches.join(', ')
+
+if (includeBranches) {
+ if (!doCheckBranchIncluded(includeBranches, eventBranches)) {
+ return [
+ responseMessage: "Ignoring '${event}' event for branch '${branchName}'",
+ launchWebhook : false
+ ]
+ }
+}
+if (excludeBranches) {
+ if (doCheckBranchIncluded(excludeBranches, eventBranches)) {
+ return [
+ responseMessage: "Ignoring '${event}' event for exluded branch '${branchName}'",
+ launchWebhook : false
+ ]
+ }
+}
+
+// Collect data for response
+Map webhookData = webhookEvent.collectWebhookData()
+Map recentCommit = webhookEvent.getRecentCommit()
+
+Map response = [
+ eventType : event,
+ launchWebhook: true,
+ branch : webhookEvent.getBranchNames().join(', ')
+] as Map
+
+if (webhookData) {
+ response['webhookData'] = webhookData
+}
+
+if (recentCommit) {
+ response['commitId'] = recentCommit['commitId']
+ response['commitAuthorName'] = recentCommit['commitAuthorName']
+ response['commitAuthorEmail'] = recentCommit['commitAuthorEmail']
+}
+
+return response
+
+/**
+ * These methods depend on the form declaration
+ */
+
+private static boolean doCheckRepositoryIncluded(String parameterValue, String checked) {
+ ArrayList list = parameterValue.tokenize("\n").collect({ it.trim() })
+ return list.contains(checked)
+}
+
+private static boolean doCheckActionIncluded(String parameterValue, String checked) {
+ if (!parameterValue) return true
+ ArrayList list = parameterValue.tokenize(/,\s+?/).collect({ it.trim() })
+ return list.contains(checked)
+}
+
+private static boolean doCheckBranchIncluded(String parameterValue, ArrayList checked) {
+ ArrayList list = parameterValue.tokenize(/,\s+?/).collect({ it.trim() })
+ for (String b : checked) {
+ if (listContainsGlobMatch(list, b)) {
+ return true
+ }
+ }
+ return false
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////
+// End of business logic
+//////////////////////////////////////////////////////////////////////////////////////////////
+
+static boolean verifySignedPayload(String remoteSignature, String secretToken, String payload) {
+ def signature = 'sha1=' + hmacSignature(payload, secretToken)
+ return signature.equals(remoteSignature)
+}
+
+static String hmacSignature(String data, String key) {
+ try {
+ final SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(UTF_8), "HmacSHA1")
+ final Mac mac = Mac.getInstance("HmacSHA1")
+ mac.init(keySpec)
+ final byte[] rawHMACBytes = mac.doFinal(data.getBytes(UTF_8) as byte[])
+
+ return Hex.encodeHexString(rawHMACBytes)
+ } catch (Exception e) {
+ throw new RuntimeException("Computed invalid signature: " + e.getMessage())
+ }
+}
+
+static boolean listContainsGlobMatch(ArrayList list, String checked) {
+ for (String l : list) {
+ def pattern = Pattern.compile(l)
+ if (pattern.matcher(checked).lookingAt()) {
+ return true
+ }
+ }
+ return false
+}
+
+abstract class WebhookEvent {
+ abstract String name
+ abstract String body
+
+ @Lazy
+ String action = {
+ payload.get('action')
+ }()
+
+ Map payload
+
+ WebhookEvent(String payload) {
+ this.body = payload
+ this.payload = (new JsonSlurper()).parseText(this.body) as Map
+ }
+
+ static WebhookEvent getForType(String event, String payload) {
+ if (event == 'ping') {
+ return new PingEvent(payload)
+ } else if (event == 'pull_request') {
+ return new PullRequestEvent(payload)
+ } else if (event == 'push') {
+ return new PushEvent(payload)
+ } else if (event == 'status') {
+ return new CommitStatusEvent(payload)
+ } else {
+ return null
+ }
+ }
+
+ String getRepositoryName() { return payload.get('repository')?.get('full_name') }
+
+ abstract ArrayList getBranchNames()
+
+ abstract Map getRecentCommit()
+
+ abstract Map collectWebhookData()
+
+}
+
+class PingEvent extends WebhookEvent {
+ static String name = 'ping'
+
+ PingEvent(String payload) {
+ super(payload)
+ }
+
+ @Override
+ ArrayList getBranchNames() {
+ return null
+ }
+
+ @Override
+ Map getRecentCommit() {
+ return null
+ }
+
+ @Override
+ Map collectWebhookData() {
+ return null
+ }
+}
+
+class PullRequestEvent extends WebhookEvent {
+
+ static String name = 'pull_request'
+
+ @Lazy
+ ArrayList branchNames = {
+ String refName = payload.get('pull_request')?.get('head')?.get('ref')
+ if (!refName) {
+ return null
+ }
+ return [refName.replace('/refs/heads/', '')]
+ }()
+
+ @Lazy
+ ArrayList