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