Browse Source

Improve Deployment Pipeline/Gerrit feedback

Writes and posts easy-to-understand Gerrit comments from the Deployment
Pipeline.

This is the alternative to the current circuitous path of
links users of the pipeline are meant to follow to find basic
information about build status, Docker images, and Docker tags.

Adds two new classes that are meant to be invoked from inside a Jenkins
job defined as a Jenkins Pipeline script. By providing access to a few
build parameters from within a Jenkins job, this patch will post a
comment to Gerrit using an output format that will be styled by Gerrit
commentstyles implemented in I5b04aa10d54b6f2587da196c02ebff9bfe5ba166.

Example posted message:

    pipeline-dashboard: service-pipeline
    pipeline-build-result: SUCCESS (job: service-pipeline, build: 27)
    IMAGE:
     docker-registry.wikimedia.org/wikimedia/mediawiki-services-citoid

    TAGS:
     test, latest

Change-Id: I2fc0924996eb1a969fcbf41bac333d3c35cd34ea
Tyler Cipriani 8 months ago
parent
commit
43b64f2c0b

+ 21
- 0
Makefile View File

@@ -0,0 +1,21 @@
1
+SHELL := /bin/bash
2
+GRADLE := $(shell command -v gradle)
3
+BLUBBER := $(shell command -v blubber)
4
+DOCKER := $(shell command -v docker)
5
+DOCKER_TAG := piplinelib-tests-$(shell date -I)
6
+
7
+
8
+.PHONY: test
9
+test:
10
+ifneq (,$(GRADLE))
11
+	gradle test
12
+	@exit 0
13
+else ifneq (,$(and $(BLUBBER), $(DOCKER)))
14
+	blubber .pipeline/blubber.yaml test | docker build -t "$(DOCKER_TAG)" -f - .
15
+    docker run --rm -it "$(DOCKER_TAG)"
16
+	docker rmi "$(DOCKER_TAG)"
17
+	@exit 0
18
+else
19
+	@echo "Can't find Gradle or Blubber/Docker. Install one to run tests."
20
+	@exit 1
21
+endif

+ 13
- 0
README.md View File

@@ -0,0 +1,13 @@
1
+# README for pipelinelib
2
+
3
+`pipelinelib` is a library built for use in the [Wikimedia Deployment
4
+Pipeline][pipeline]. It is a library of Groovy code that is loaded by Jenkins
5
+to execute tasks related to the Pipeline.
6
+
7
+[pipeline]: https://wikitech.wikimedia.org/wiki/Deployment_pipeline
8
+
9
+## Running Tests
10
+
11
+To run tests use GNU Make:
12
+
13
+    make test

+ 8
- 0
src/org/wikimedia/integration/GerritComment.groovy View File

@@ -0,0 +1,8 @@
1
+package org.wikimedia.integration
2
+
3
+/**
4
+ * Abstract class to represent the contract of a gerrit comment
5
+ */
6
+abstract class GerritComment {
7
+    String formatMessage
8
+}

+ 82
- 0
src/org/wikimedia/integration/GerritPipelineComment.groovy View File

@@ -0,0 +1,82 @@
1
+package org.wikimedia.integration
2
+
3
+/**
4
+ * Gerrit revision that can be used to comment on a gerrit patchset
5
+ *
6
+ * {@code
7
+ * import org.wikimedia.integration.GerritReview
8
+ * import org.wikimedia.integration.GerritPipelineComment
9
+ *
10
+ * stage('comment') {
11
+ *   comment = new GerritPipelineComment(
12
+ *     jobName: xx,
13
+ *     buildNumber: xx,
14
+ *     jobStatus: xx,
15
+ *     image: xx,
16
+ *     tags: xx
17
+ *   )
18
+ *   GerritReview.post(this, comment)
19
+ * }
20
+ */
21
+class GerritPipelineComment extends GerritComment implements Serializable {
22
+  /**
23
+   * Name of the job
24
+   */
25
+  String jobName
26
+
27
+  /**
28
+   * Jenkins build number
29
+   */
30
+  String buildNumber
31
+
32
+  /**
33
+   * Image in the docker registry
34
+   */
35
+  String image
36
+
37
+  /**
38
+   * Build status
39
+   */
40
+  String jobStatus
41
+
42
+  /**
43
+   * Image tags
44
+   */
45
+  List<String> tags
46
+
47
+  String formatDashboard() {
48
+    "pipeline-dashboard: ${this.jobName}"
49
+  }
50
+
51
+  String formatResult() {
52
+    "pipeline-build-result: ${this.jobStatus} (job: ${this.jobName}, build: ${this.buildNumber})"
53
+  }
54
+
55
+  String formatImage() {
56
+    "IMAGE:\n ${this.image}"
57
+  }
58
+
59
+  String formatTags() {
60
+    "TAGS:\n ${this.tags.join(', ')}"
61
+  }
62
+
63
+  /**
64
+   * Format final message output
65
+   */
66
+  String formatMessage() {
67
+    def msg = "${this.formatDashboard()}\n${this.formatResult()}\n"
68
+
69
+    if (this.image != null) {
70
+      msg = "${msg}\n${this.formatImage()}\n"
71
+    }
72
+    if (this.tags != null) {
73
+      msg = "${msg}\n${this.formatTags()}\n"
74
+    }
75
+
76
+    msg
77
+  }
78
+
79
+  GerritPipelineComment(Map settings = [:]) {
80
+    settings.each { prop, value -> this.@"${prop}" = value }
81
+  }
82
+}

+ 119
- 0
src/org/wikimedia/integration/GerritReview.groovy View File

@@ -0,0 +1,119 @@
1
+package org.wikimedia.integration
2
+
3
+import java.net.URLEncoder
4
+import groovy.json.JsonOutput
5
+
6
+/**
7
+ * Gerrit review that can be used to comment on a gerrit patchset
8
+ *
9
+ * {@code
10
+ * import org.wikimedia.integration.GerritReview
11
+ * import org.wikimedia.integration.GerritComment
12
+ *
13
+ * stage('comment') {
14
+ *   comment = new GerritComment(
15
+ *     jobName: xx,
16
+ *     buildNumber: xx,
17
+ *     jobStatus: xx,
18
+ *     image: xx,
19
+ *     tags: xx
20
+ *   )
21
+ *   GerritReview.post(this, comment)
22
+ * }
23
+ * }
24
+ */
25
+
26
+class GerritReview implements Serializable {
27
+  /**
28
+   * Jenkins pipeline workflow script context.
29
+   */
30
+  final def workflowScript
31
+
32
+  /**
33
+   * Gerrit URL
34
+   */
35
+  final def gerritURL = 'https://gerrit.wikimedia.org/r'
36
+
37
+  /**
38
+   * Name of the auth credentials in jenkins to use in gerrit
39
+   */
40
+  final def gerritAuthCreds = 'gerrit.pipelinebot'
41
+
42
+  /**
43
+   * GerritComment with information for message body
44
+   */
45
+  final GerritComment comment
46
+
47
+  GerritReview(workflowScript, GerritComment comment) {
48
+    this.workflowScript = workflowScript
49
+    this.comment = comment
50
+  }
51
+
52
+  /**
53
+   * URLEncoded ZUUL_PROJECT from the environment.
54
+   *
55
+   * This should be set for all patchsets.
56
+   */
57
+  String getProject() {
58
+    URLEncoder.encode(this.workflowScript.env.ZUUL_PROJECT, 'UTF-8')
59
+  }
60
+
61
+  /**
62
+   * Return a full authorized url for a gerrit revision review.
63
+   *
64
+   * May return an empty string in the cases of an unexpected environment.
65
+   */
66
+  String getRequestURL() {
67
+    def change = this.workflowScript.env.ZUUL_CHANGE
68
+    def revision = this.workflowScript.env.ZUUL_PATCHSET
69
+
70
+    if (! revision || ! change) { return "" }
71
+
72
+    def changeId = [this.getProject(), change].join('~')
73
+
74
+
75
+    [
76
+      this.gerritURL,
77
+      'a/changes',
78
+      changeId,
79
+      'revisions',
80
+      revision,
81
+      'review',
82
+    ].join('/')
83
+  }
84
+
85
+  /**
86
+   * Format gerritcomment as a json message.
87
+   */
88
+  String getBody() {
89
+    JsonOutput.toJson([message: this.comment.formatMessage()])
90
+  }
91
+
92
+ /**
93
+  * Static method to POST GerritComment to a particular change.
94
+  *
95
+  * Uses the Gerrit RESTAPI to POST a comment on a patchset revision.
96
+  *
97
+  * @param workflowScript Jenkins workflow script context.
98
+  * @param comment GerritComment
99
+  */
100
+  static String post(workflowScript, GerritComment comment) {
101
+    def gr = new GerritReview(workflowScript, comment)
102
+    def url = gr.getRequestURL()
103
+
104
+    if (! url) {
105
+      gr.workflowScript.error "Could not determine Gerrit ChangeID from Environment. Aborting."
106
+    }
107
+
108
+    def response = gr.workflowScript.httpRequest(
109
+      url: url,
110
+      httpMode: 'POST',
111
+      customHeaders: [[name: "content-type", value: 'application/json']],
112
+      requestBody: gr.getBody(),
113
+      consoleLogResponseBody: true,
114
+      validResponseCodes: "200",
115
+      authentication: gr.gerritAuthCreds
116
+    )
117
+    response.content
118
+  }
119
+}

+ 79
- 0
test/org/wikimedia/integration/GerritPipelineCommentTest.groovy View File

@@ -0,0 +1,79 @@
1
+import groovy.util.GroovyTestCase
2
+
3
+import org.wikimedia.integration.GerritPipelineComment
4
+
5
+class GerritCommentTestCase extends GroovyTestCase {
6
+  private GerritPipelineComment gerritComment
7
+
8
+  void testGetDashboardOutput() {
9
+    gerritComment = new GerritPipelineComment(
10
+      jobName: "service-pipeline-test-and-publish"
11
+    )
12
+    assert gerritComment.formatDashboard() == 'pipeline-dashboard: service-pipeline-test-and-publish'
13
+  }
14
+
15
+  void testGetResultOutput() {
16
+    gerritComment = new GerritPipelineComment(
17
+      jobName: 'service-pipeline-test-and-publish',
18
+      jobStatus: 'SUCCESS',
19
+      buildNumber: '25'
20
+    )
21
+    assert gerritComment.formatResult() == \
22
+      'pipeline-build-result: SUCCESS (job: service-pipeline-test-and-publish, build: 25)'
23
+  }
24
+
25
+  void testGetFormatImage() {
26
+    def imageName = 'docker-registry.wikimedia.org/wikimedia/mediawiki-services-citoid'
27
+    def expected = "IMAGE:\n ${imageName}"
28
+    gerritComment = new GerritPipelineComment(image: imageName)
29
+    assert gerritComment.formatImage() == expected
30
+  }
31
+
32
+  void testGetFormatTags() {
33
+    def tags = ['2019-02-11-214153-production', 'fc52e49b051872b282c6a66be6649c7d437bf066']
34
+    def expected = "TAGS:\n 2019-02-11-214153-production, fc52e49b051872b282c6a66be6649c7d437bf066"
35
+    gerritComment = new GerritPipelineComment(tags: tags)
36
+    assert gerritComment.formatTags() == expected
37
+  }
38
+
39
+  void testwithoutImage() {
40
+    def expected = '''\
41
+    pipeline-dashboard: service-pipeline-test-and-publish
42
+    pipeline-build-result: SUCCESS (job: service-pipeline-test-and-publish, build: 25)
43
+    '''.stripIndent()
44
+
45
+    gerritComment = new GerritPipelineComment(
46
+      jobName: 'service-pipeline-test-and-publish',
47
+      jobStatus: 'SUCCESS',
48
+      buildNumber: '25',
49
+    )
50
+
51
+    assert gerritComment.formatMessage() == expected
52
+  }
53
+
54
+  void testwithImage() {
55
+    def tags = ['2019-02-11-214153-production', 'fc52e49b051872b282c6a66be6649c7d437bf066']
56
+    def imageName = 'docker-registry.wikimedia.org/wikimedia/mediawiki-services-citoid'
57
+
58
+    def expected = '''\
59
+    pipeline-dashboard: service-pipeline-test-and-publish
60
+    pipeline-build-result: SUCCESS (job: service-pipeline-test-and-publish, build: 25)
61
+
62
+    IMAGE:
63
+     docker-registry.wikimedia.org/wikimedia/mediawiki-services-citoid
64
+
65
+    TAGS:
66
+     2019-02-11-214153-production, fc52e49b051872b282c6a66be6649c7d437bf066
67
+    '''.stripIndent()
68
+
69
+    gerritComment = new GerritPipelineComment(
70
+      jobName: 'service-pipeline-test-and-publish',
71
+      jobStatus: 'SUCCESS',
72
+      buildNumber: '25',
73
+      image: imageName,
74
+      tags: tags
75
+    )
76
+
77
+    assert gerritComment.formatMessage() == expected
78
+  }
79
+}

+ 38
- 0
test/org/wikimedia/integration/GerritReviewTest.groovy View File

@@ -0,0 +1,38 @@
1
+import groovy.util.GroovyTestCase
2
+
3
+import org.wikimedia.integration.GerritReview
4
+import org.wikimedia.integration.GerritPipelineComment
5
+
6
+class GerritReviewTestCase extends GroovyTestCase {
7
+  private class Env {
8
+    String ZUUL_PATCHSET = '8'
9
+    String ZUUL_CHANGE = '486851'
10
+    String ZUUL_PROJECT = 'mediawiki/services/citoid'
11
+  }
12
+
13
+  private class WorkflowScript {
14
+    Env env
15
+    WorkflowScript(env) { this.env = env }
16
+  }
17
+
18
+  void testReviewURL() {
19
+    def gr = new GerritReview(new WorkflowScript(new Env()), new GerritPipelineComment())
20
+    assert gr.getProject() == 'mediawiki%2Fservices%2Fcitoid'
21
+    assert gr.getRequestURL() == 'https://gerrit.wikimedia.org/r/a/changes/mediawiki%2Fservices%2Fcitoid~486851/revisions/8/review'
22
+  }
23
+
24
+  void testReviewBody() {
25
+    def expected = '{"message":"pipeline-dashboard: service-pipeline-test-and-publish\\n'
26
+    expected += 'pipeline-build-result: SUCCESS '
27
+    expected += '(job: service-pipeline-test-and-publish, build: 25)\\n"}'
28
+
29
+    def gerritComment = new GerritPipelineComment(
30
+      jobName: 'service-pipeline-test-and-publish',
31
+      jobStatus: 'SUCCESS',
32
+      buildNumber: '25',
33
+    )
34
+
35
+    def gr = new GerritReview(new WorkflowScript(new Env()), gerritComment)
36
+    assert gr.getBody() == expected
37
+  }
38
+}

Loading…
Cancel
Save