Skip to content

Commit 35c27d5

Browse files
authored
Add events workflow (#130)
1 parent 8c5de7b commit 35c27d5

16 files changed

+91872
-2
lines changed

.isort.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[settings]
2-
known_first_party = code_coverage_backend,code_coverage_bot,code_coverage_tools,conftest
3-
known_third_party = connexion,datadog,dateutil,fakeredis,flask,flask_cors,flask_talisman,google,hglib,jsone,jsonschema,libmozdata,logbook,pytest,pytz,raven,redis,requests,responses,setuptools,structlog,taskcluster,werkzeug,zstandard
2+
known_first_party = code_coverage_backend,code_coverage_bot,code_coverage_events,code_coverage_tools,conftest
3+
known_third_party = connexion,datadog,dateutil,fakeredis,flask,flask_cors,flask_talisman,google,hglib,jsone,jsonschema,libmozdata,libmozevent,logbook,pytest,pytz,raven,redis,requests,responses,setuptools,structlog,taskcluster,werkzeug,zstandard
44
force_single_line = True
55
default_section=FIRSTPARTY
66
line_length=159

.taskcluster.yml

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,26 @@ tasks:
116116
117117
source: https://github.com/mozilla/code-coverage
118118

119+
- taskId: {$eval: as_slugid("events_check_tests")}
120+
provisionerId: aws-provisioner-v1
121+
workerType: github-worker
122+
created: {$fromNow: ''}
123+
deadline: {$fromNow: '1 hour'}
124+
payload:
125+
maxRunTime: 3600
126+
image: python:3
127+
command:
128+
- sh
129+
- -lxce
130+
- "git clone --quiet ${repository} /src && cd /src && git checkout ${head_rev} -b checks &&
131+
cd /src/events && pip install -q -r requirements.txt && pip install --quiet . && pip install --quiet -r requirements-dev.txt &&
132+
pytest -v"
133+
metadata:
134+
name: "Code Coverage Events checks: unit tests"
135+
description: Check python code with pytest
136+
137+
source: https://github.com/mozilla/code-coverage
138+
119139
- taskId: {$eval: as_slugid("backend_build")}
120140
created: {$fromNow: ''}
121141
deadline: {$fromNow: '1 hour'}
@@ -198,6 +218,47 @@ tasks:
198218
199219
source: https://github.com/mozilla/code-coverage
200220

221+
- taskId: {$eval: as_slugid("events_build")}
222+
created: {$fromNow: ''}
223+
deadline: {$fromNow: '1 hour'}
224+
provisionerId: aws-provisioner-v1
225+
workerType: releng-svc
226+
dependencies:
227+
- {$eval: as_slugid("check_lint")}
228+
- {$eval: as_slugid("events_check_tests")}
229+
payload:
230+
capabilities:
231+
privileged: true
232+
maxRunTime: 3600
233+
image: "${taskboot_image}"
234+
env:
235+
GIT_REPOSITORY: ${repository}
236+
GIT_REVISION: ${head_rev}
237+
command:
238+
- taskboot
239+
- build
240+
- --image
241+
- mozilla/code-coverage
242+
- --tag
243+
- "events-${channel}"
244+
- --tag
245+
- "events-${head_rev}"
246+
- --write
247+
- /events.tar
248+
- events/Dockerfile
249+
artifacts:
250+
public/code-coverage-events.tar:
251+
expires: {$fromNow: '2 weeks'}
252+
path: /events.tar
253+
type: file
254+
scopes:
255+
- docker-worker:capability:privileged
256+
metadata:
257+
name: Code Coverage Events docker build
258+
description: Build docker image with taskboot
259+
260+
source: https://github.com/mozilla/code-coverage
261+
201262
- taskId: {$eval: as_slugid("addon_build")}
202263
provisionerId: aws-provisioner-v1
203264
workerType: github-worker
@@ -282,6 +343,36 @@ tasks:
282343
283344
source: https://github.com/mozilla/code-coverage
284345

346+
- $if: 'channel in ["testing", "production"]'
347+
then:
348+
taskId: {$eval: as_slugid("events_deploy")}
349+
created: {$fromNow: ''}
350+
deadline: {$fromNow: '1 hour'}
351+
provisionerId: aws-provisioner-v1
352+
workerType: github-worker
353+
dependencies:
354+
- {$eval: as_slugid("events_build")}
355+
payload:
356+
features:
357+
taskclusterProxy: true
358+
maxRunTime: 3600
359+
image: "${taskboot_image}"
360+
command:
361+
- taskboot
362+
- deploy-heroku
363+
- --heroku-app
364+
- "code-coverage-events-${channel}"
365+
- worker:public/code-coverage-events.tar
366+
env:
367+
TASKCLUSTER_SECRET: "project/relman/code-coverage/deploy-${channel}"
368+
scopes:
369+
- "secrets:get:project/relman/code-coverage/deploy-${channel}"
370+
metadata:
371+
name: "Code Coverage Events deployment (${channel})"
372+
description: Deploy docker image on Heroku
373+
374+
source: https://github.com/mozilla/code-coverage
375+
285376
- $if: 'channel in ["testing", "production"]'
286377
then:
287378
taskId: {$eval: as_slugid("bot_deploy")}

events/Dockerfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FROM python:3-slim
2+
3+
ADD events /src
4+
5+
RUN cd /src && pip install --disable-pip-version-check --no-cache-dir -r requirements.txt && python setup.py install
6+
7+
CMD ["code-coverage-events"]

events/VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.0.0
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# -*- coding: utf-8 -*-
2+
3+
QUEUE_MONITORING = "monitoring"
4+
QUEUE_PULSE = "pulse"

events/code_coverage_events/cli.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# -*- coding: utf-8 -*-
2+
import argparse
3+
import os
4+
5+
import structlog
6+
from libmozevent import taskcluster_config
7+
from libmozevent.log import init_logger
8+
9+
from code_coverage_events.workflow import Events
10+
11+
logger = structlog.get_logger(__name__)
12+
13+
14+
def parse_cli():
15+
"""
16+
Setup CLI options parser
17+
"""
18+
parser = argparse.ArgumentParser(description="Mozilla Code Review Bot")
19+
parser.add_argument(
20+
"--taskcluster-secret",
21+
help="Taskcluster Secret path",
22+
default=os.environ.get("TASKCLUSTER_SECRET"),
23+
)
24+
parser.add_argument("--taskcluster-client-id", help="Taskcluster Client ID")
25+
parser.add_argument("--taskcluster-access-token", help="Taskcluster Access token")
26+
return parser.parse_args()
27+
28+
29+
def main():
30+
args = parse_cli()
31+
taskcluster_config.auth(args.taskcluster_client_id, args.taskcluster_access_token)
32+
taskcluster_config.load_secrets(
33+
args.taskcluster_secret,
34+
"events",
35+
required=("pulse_user", "pulse_password", "hook_id", "hook_group_id"),
36+
existing=dict(admins=["[email protected]", "[email protected]"]),
37+
)
38+
39+
init_logger(
40+
"code_coverage_events",
41+
PAPERTRAIL_HOST=taskcluster_config.secrets.get("PAPERTRAIL_HOST"),
42+
PAPERTRAIL_PORT=taskcluster_config.secrets.get("PAPERTRAIL_PORT"),
43+
SENTRY_DSN=taskcluster_config.secrets.get("SENTRY_DSN"),
44+
)
45+
46+
events = Events()
47+
events.run()
48+
49+
50+
if __name__ == "__main__":
51+
main()
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import asyncio
4+
5+
import requests
6+
import structlog
7+
from libmozevent import taskcluster_config
8+
from libmozevent.bus import MessageBus
9+
from libmozevent.monitoring import Monitoring
10+
from libmozevent.pulse import PulseListener
11+
from libmozevent.utils import retry
12+
from libmozevent.utils import run_tasks
13+
14+
from code_coverage_events import QUEUE_MONITORING
15+
from code_coverage_events import QUEUE_PULSE
16+
17+
logger = structlog.get_logger(__name__)
18+
19+
20+
class CodeCoverage(object):
21+
"""
22+
Taskcluster hook handling the code coverage
23+
"""
24+
25+
def __init__(self, hook_id, hook_group_id, bus):
26+
self.triggered_groups = set()
27+
self.group_id = hook_group_id
28+
self.hook_id = hook_id
29+
self.bus = bus
30+
31+
# Setup TC services
32+
self.queue = taskcluster_config.get_service("queue")
33+
self.hooks = taskcluster_config.get_service("hooks")
34+
35+
async def run(self):
36+
"""
37+
Main consumer, running queued payloads from the pulse listener
38+
"""
39+
while True:
40+
# Get next payload from pulse messages
41+
payload = await self.bus.receive(QUEUE_PULSE)
42+
43+
# Parse the payload to extract a new task's environment
44+
envs = await self.parse(payload)
45+
if envs is None:
46+
continue
47+
48+
for env in envs:
49+
# Trigger new tasks
50+
task = self.hooks.triggerHook(self.group_id, self.hook_id, env)
51+
task_id = task["status"]["taskId"]
52+
logger.info("Triggered a new code coverage task", id=task_id)
53+
54+
# Send task to monitoring
55+
await self.bus.send(
56+
QUEUE_MONITORING, (self.group_id, self.hook_id, task_id)
57+
)
58+
59+
def is_coverage_task(self, task):
60+
return any(
61+
task["task"]["metadata"]["name"].startswith(s)
62+
for s in ["build-linux64-ccov", "build-win64-ccov"]
63+
)
64+
65+
async def get_build_task_in_group(self, group_id):
66+
if group_id in self.triggered_groups:
67+
logger.info(
68+
"Received duplicated groupResolved notification", group=group_id
69+
)
70+
return None
71+
72+
def maybe_trigger(tasks):
73+
logger.info(
74+
"Checking code coverage tasks", group_id=group_id, nb=len(tasks)
75+
)
76+
for task in tasks:
77+
if self.is_coverage_task(task):
78+
self.triggered_groups.add(group_id)
79+
return task
80+
81+
return None
82+
83+
def load_tasks(limit=200, continuationToken=None):
84+
query = {"limit": limit}
85+
if continuationToken is not None:
86+
query["continuationToken"] = continuationToken
87+
reply = retry(lambda: self.queue.listTaskGroup(group_id, query=query))
88+
return maybe_trigger(reply["tasks"]), reply.get("continuationToken")
89+
90+
async def retrieve_coverage_task():
91+
task, token = load_tasks()
92+
93+
while task is None and token is not None:
94+
task, token = load_tasks(continuationToken=token)
95+
96+
# Let other tasks run on long batches
97+
await asyncio.sleep(0)
98+
99+
return task
100+
101+
try:
102+
return await retrieve_coverage_task()
103+
except requests.exceptions.HTTPError:
104+
return None
105+
106+
async def parse(self, body):
107+
"""
108+
Extract revisions from payload
109+
"""
110+
taskGroupId = body["taskGroupId"]
111+
112+
build_task = await self.get_build_task_in_group(taskGroupId)
113+
if build_task is None:
114+
return None
115+
116+
repository = build_task["task"]["payload"]["env"]["GECKO_HEAD_REPOSITORY"]
117+
118+
if repository not in [
119+
"https://hg.mozilla.org/mozilla-central",
120+
"https://hg.mozilla.org/try",
121+
]:
122+
logger.warn(
123+
"Received groupResolved notification for a coverage task in an unexpected branch",
124+
repository=repository,
125+
)
126+
return None
127+
128+
logger.info(
129+
"Received groupResolved notification for coverage builds",
130+
repository=repository,
131+
revision=build_task["task"]["payload"]["env"]["GECKO_HEAD_REV"],
132+
group=taskGroupId,
133+
)
134+
135+
return [
136+
{
137+
"REPOSITORY": repository,
138+
"REVISION": build_task["task"]["payload"]["env"]["GECKO_HEAD_REV"],
139+
}
140+
]
141+
142+
143+
class Events(object):
144+
"""
145+
Listen to pulse events and trigger new code coverage tasks
146+
"""
147+
148+
def __init__(self):
149+
# Create message bus shared amongst process
150+
self.bus = MessageBus()
151+
152+
# Build code coverage workflow
153+
self.workflow = CodeCoverage(
154+
taskcluster_config.secrets["hook_id"],
155+
taskcluster_config.secrets["hook_group_id"],
156+
self.bus,
157+
)
158+
159+
# Setup monitoring for newly created tasks
160+
self.monitoring = Monitoring(
161+
QUEUE_MONITORING, taskcluster_config.secrets["admins"], 7 * 3600
162+
)
163+
self.monitoring.register(self.bus)
164+
165+
# Create pulse listener for code coverage
166+
self.pulse = PulseListener(
167+
QUEUE_PULSE,
168+
"exchange/taskcluster-queue/v1/task-group-resolved",
169+
"#",
170+
taskcluster_config.secrets["pulse_user"],
171+
taskcluster_config.secrets["pulse_password"],
172+
)
173+
self.pulse.register(self.bus)
174+
175+
def run(self):
176+
177+
consumers = [
178+
# Code coverage main workflow
179+
self.workflow.run(),
180+
# Add monitoring task
181+
self.monitoring.run(),
182+
# Add pulse task
183+
self.pulse.run(),
184+
]
185+
186+
# Run all tasks concurrently
187+
run_tasks(consumers)

events/requirements-dev.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pytest
2+
pytest-asyncio
3+
responses

events/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
libmozevent==1.0.0

0 commit comments

Comments
 (0)