]> git.immae.eu Git - perso/Immae/Config/Nix.git/blob - nixops/modules/buildbot/common/build_helpers.py
Add xmpp push notifications to buildbot and gitolite
[perso/Immae/Config/Nix.git] / nixops / modules / buildbot / common / build_helpers.py
1 from buildbot.plugins import util, steps, schedulers
2 from buildbot_buildslist import BuildsList
3
4 __all__ = [
5 "force_scheduler", "deploy_scheduler", "hook_scheduler",
6 "clean_branch", "package_and_upload", "SlackStatusPush",
7 "XMPPStatusPush"
8 ]
9
10 # Small helpers"
11 @util.renderer
12 def clean_branch(props):
13 if props.hasProperty("branch") and len(props["branch"]) > 0:
14 return props["branch"].replace("/", "_")
15 else:
16 return "HEAD"
17
18 def package_and_upload(package, package_dest, package_url):
19 return [
20 steps.ShellCommand(name="build package",
21 logEnviron=False, haltOnFailure=True, workdir="source",
22 command=["git", "archive", "HEAD", "-o", package]),
23
24 steps.FileUpload(name="upload package", workersrc=package,
25 workdir="source", masterdest=package_dest,
26 url=package_url, mode=0o644),
27
28 steps.ShellCommand(name="cleanup package", logEnviron=False,
29 haltOnFailure=True, workdir="source", alwaysRun=True,
30 command=["rm", "-f", package]),
31 ]
32
33 # Schedulers
34 def force_scheduler(name, builders):
35 return schedulers.ForceScheduler(name=name,
36 label="Force build", buttonName="Force build",
37 reason=util.StringParameter(name="reason", label="Reason", default="Force build"),
38 codebases=[
39 util.CodebaseParameter("",
40 branch=util.StringParameter(
41 name="branch", label="Git reference (tag, branch)", required=True),
42 revision=util.FixedParameter(name="revision", default=""),
43 repository=util.FixedParameter(name="repository", default=""),
44 project=util.FixedParameter(name="project", default=""),
45 ),
46 ],
47 username=util.FixedParameter(name="username", default="Web button"),
48 builderNames=builders)
49
50 def deploy_scheduler(name, builders):
51 return schedulers.ForceScheduler(name=name,
52 builderNames=builders,
53 label="Deploy built package", buttonName="Deploy",
54 username=util.FixedParameter(name="username", default="Web button"),
55 codebases=[
56 util.CodebaseParameter(codebase="",
57 branch=util.FixedParameter(name="branch", default=""),
58 revision=util.FixedParameter(name="revision", default=""),
59 repository=util.FixedParameter(name="repository", default=""),
60 project=util.FixedParameter(name="project", default=""))],
61 reason=util.FixedParameter(name="reason", default="Deploy"),
62 properties=[
63 util.ChoiceStringParameter(label="Environment",
64 name="environment", default="integration",
65 choices=["integration", "production"]),
66 BuildsList(label="Build to deploy", name="build"),
67 ]
68 )
69
70 def hook_scheduler(project, timer=10):
71 return schedulers.AnyBranchScheduler(
72 change_filter=util.ChangeFilter(category="hooks", project=project),
73 name=project, treeStableTimer=timer, builderNames=["{}_build".format(project)])
74
75 # Slack/XMPP status push
76 from buildbot.reporters.http import HttpStatusPushBase
77 from twisted.internet import defer
78 from twisted.python import log
79 from buildbot.util import httpclientservice
80 from buildbot.reporters import utils
81 from buildbot.process import results
82 from twisted.words.protocols.jabber.jid import JID
83 from wokkel import client, xmppim
84
85
86 class SlackStatusPush(HttpStatusPushBase):
87 name = "SlackStatusPush"
88
89 @defer.inlineCallbacks
90 def reconfigService(self, serverUrl, **kwargs):
91 yield HttpStatusPushBase.reconfigService(self, **kwargs)
92 self._http = yield httpclientservice.HTTPClientService.getService(
93 self.master, serverUrl)
94
95 @defer.inlineCallbacks
96 def send(self, build):
97 yield utils.getDetailsForBuild(self.master, build, wantProperties=True)
98 response = yield self._http.post("", json=self.format(build))
99 if response.code != 200:
100 log.msg("%s: unable to upload status: %s" %
101 (response.code, response.content))
102
103 def format(self, build):
104 colors = [
105 "#36A64F", # success
106 "#F1E903", # warnings
107 "#DA0505", # failure
108 "#FFFFFF", # skipped
109 "#000000", # exception
110 "#FFFFFF", # retry
111 "#D02CA9", # cancelled
112 ]
113
114 if "environment" in build["properties"]:
115 msg = "{} environment".format(build["properties"]["environment"][0])
116 if "build" in build["properties"]:
117 msg = "of archive {} in ".format(build["properties"]["build"][0]) + msg
118 elif len(build["buildset"]["sourcestamps"][0]["branch"]) > 0:
119 msg = "revision {}".format(build["buildset"]["sourcestamps"][0]["branch"])
120 else:
121 msg = "build"
122
123 if build["complete"]:
124 timedelta = int((build["complete_at"] - build["started_at"]).total_seconds())
125 hours, rest = divmod(timedelta, 3600)
126 minutes, seconds = divmod(rest, 60)
127 if hours > 0:
128 duration = "{}h {}min {}s".format(hours, minutes, seconds)
129 elif minutes > 0:
130 duration = "{}min {}s".format(minutes, seconds)
131 else:
132 duration = "{}s".format(seconds)
133
134 text = "Build <{}|{}> of {}'s {} was {} in {}.".format(
135 build["url"], build["buildid"],
136 build["builder"]["name"],
137 msg,
138 results.Results[build["results"]],
139 duration,
140 )
141 fields = [
142 {
143 "title": "Build",
144 "value": "<{}|{}>".format(build["url"], build["buildid"]),
145 "short": True,
146 },
147 {
148 "title": "Project",
149 "value": build["builder"]["name"],
150 "short": True,
151 },
152 {
153 "title": "Build status",
154 "value": results.Results[build["results"]],
155 "short": True,
156 },
157 {
158 "title": "Build duration",
159 "value": duration,
160 "short": True,
161 },
162 ]
163 if "environment" in build["properties"]:
164 fields.append({
165 "title": "Environment",
166 "value": build["properties"]["environment"][0],
167 "short": True,
168 })
169 if "build" in build["properties"]:
170 fields.append({
171 "title": "Archive",
172 "value": build["properties"]["build"][0],
173 "short": True,
174 })
175 attachments = [{
176 "fallback": "",
177 "color": colors[build["results"]],
178 "fields": fields
179 }]
180 else:
181 text = "Build <{}|{}> of {}'s {} started.".format(
182 build["url"], build["buildid"],
183 build["builder"]["name"],
184 msg,
185 )
186 attachments = []
187
188 return {
189 "username": "Buildbot",
190 "icon_url": "http://docs.buildbot.net/current/_static/icon.png",
191 "text": text,
192 "attachments": attachments,
193 }
194
195 class XMPPStatusPush(HttpStatusPushBase):
196 name = "XMPPStatusPush"
197
198 @defer.inlineCallbacks
199 def reconfigService(self, password, recipients, **kwargs):
200 yield HttpStatusPushBase.reconfigService(self, **kwargs)
201 self.password = password
202 self.recipients = recipients
203
204 @defer.inlineCallbacks
205 def send(self, build):
206 yield utils.getDetailsForBuild(self.master, build, wantProperties=True)
207 body = self.format(build)
208 factory = client.DeferredClientFactory(JID("notify_bot@immae.fr/buildbot"), self.password)
209 factory.streamManager.logTraffic = True
210 d = client.clientCreator(factory)
211 def send_message(stream):
212 message = xmppim.Message(recipient=JID(recipient), body=body)
213 message.stanzaType = 'chat'
214 stream.send(message.toElement())
215 # To allow chaining
216 return stream
217 for recipient in self.recipients:
218 d.addCallback(send_message)
219 d.addCallback(lambda _: factory.streamManager.xmlstream.sendFooter())
220 d.addErrback(log.err)
221
222 def format(self, build):
223 if "environment" in build["properties"]:
224 msg = "{} environment".format(build["properties"]["environment"][0])
225 if "build" in build["properties"]:
226 msg = "of archive {} in ".format(build["properties"]["build"][0]) + msg
227 elif len(build["buildset"]["sourcestamps"][0]["branch"]) > 0:
228 msg = "revision {}".format(build["buildset"]["sourcestamps"][0]["branch"])
229 else:
230 msg = "build"
231
232 if build["complete"]:
233 timedelta = int((build["complete_at"] - build["started_at"]).total_seconds())
234 hours, rest = divmod(timedelta, 3600)
235 minutes, seconds = divmod(rest, 60)
236 if hours > 0:
237 duration = "{}h {}min {}s".format(hours, minutes, seconds)
238 elif minutes > 0:
239 duration = "{}min {}s".format(minutes, seconds)
240 else:
241 duration = "{}s".format(seconds)
242
243 text = "Build {} ( {} ) of {}'s {} was {} in {}.".format(
244 build["buildid"], build["url"],
245 build["builder"]["name"],
246 msg,
247 results.Results[build["results"]],
248 duration,
249 )
250 else:
251 text = "Build {} ( {} ) of {}'s {} started.".format(
252 build["buildid"], build["url"],
253 build["builder"]["name"],
254 msg,
255 )
256
257 return text