blob: bc82db7f51a54de059e071e0eeededf3d6f9281d [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import click
import collections
import operator
import functools
from io import StringIO
import textwrap
# TODO(kszucs): use archery.report.JinjaReport instead
class Report:
def __init__(self, job):
self.job = job
def show(self):
raise NotImplementedError()
class ConsoleReport(Report):
"""Report the status of a Job to the console using click"""
# output table's header template
HEADER = '[{state:>7}] {branch:<52} {content:>16}'
DETAILS = ' └ {url}'
# output table's row template for assets
ARTIFACT_NAME = '{artifact:>69} '
ARTIFACT_STATE = '[{state:>7}]'
# state color mapping to highlight console output
COLORS = {
# from CombinedStatus
'error': 'red',
'failure': 'red',
'pending': 'yellow',
'success': 'green',
# custom state messages
'ok': 'green',
'missing': 'red'
}
def lead(self, state, branch, n_uploaded, n_expected):
line = self.HEADER.format(
state=state.upper(),
branch=branch,
content='uploaded {} / {}'.format(n_uploaded, n_expected)
)
return click.style(line, fg=self.COLORS[state.lower()])
def header(self):
header = self.HEADER.format(
state='state',
branch='Task / Branch',
content='Artifacts'
)
delimiter = '-' * len(header)
return '{}\n{}'.format(header, delimiter)
def artifact(self, state, pattern, asset):
if asset is None:
artifact = pattern
state = 'pending' if state == 'pending' else 'missing'
else:
artifact = asset.name
state = 'ok'
name_ = self.ARTIFACT_NAME.format(artifact=artifact)
state_ = click.style(
self.ARTIFACT_STATE.format(state=state.upper()),
self.COLORS[state]
)
return name_ + state_
def show(self, outstream, asset_callback=None):
echo = functools.partial(click.echo, file=outstream)
# write table's header
echo(self.header())
# write table's body
for task_name, task in sorted(self.job.tasks.items()):
# if not task_name.startswith("test-debian-10-python-3"):
# continue
# write summary of the uploaded vs total assets
status = task.status()
assets = task.assets()
# mapping of artifact pattern to asset or None of not uploaded
n_expected = len(task.artifacts)
n_uploaded = len(assets.uploaded_assets())
echo(self.lead(status.combined_state, task_name, n_uploaded,
n_expected))
# show link to the actual build, some of the CI providers implement
# the statuses API others implement the checks API, so display both
for link in status.build_links:
echo(self.DETAILS.format(url=link))
# write per asset status
for artifact_pattern, asset in assets.items():
if asset_callback is not None:
asset_callback(task_name, task, asset)
echo(self.artifact(status.combined_state, artifact_pattern,
asset))
class EmailReport(Report):
HEADER = textwrap.dedent("""
Arrow Build Report for Job {job_name}
All tasks: {all_tasks_url}
""")
TASK = textwrap.dedent("""
- {name}:
URL: {url}
""").strip()
EMAIL = textwrap.dedent("""
From: {sender_name} <{sender_email}>
To: {recipient_email}
Subject: {subject}
{body}
""").strip()
STATUS_HEADERS = {
# from CombinedStatus
'error': 'Errored Tasks:',
'failure': 'Failed Tasks:',
'pending': 'Pending Tasks:',
'success': 'Succeeded Tasks:',
}
def __init__(self, job, sender_name, sender_email, recipient_email):
self.sender_name = sender_name
self.sender_email = sender_email
self.recipient_email = recipient_email
super().__init__(job)
def url(self, query):
repo_url = self.job.queue.remote_url.strip('.git')
return '{}/branches/all?query={}'.format(repo_url, query)
def listing(self, tasks):
return '\n'.join(
sorted(
self.TASK.format(name=task_name, url=self.url(task.branch))
for task_name, task in tasks.items()
)
)
def header(self):
url = self.url(self.job.branch)
return self.HEADER.format(job_name=self.job.branch, all_tasks_url=url)
def subject(self):
return (
"[NIGHTLY] Arrow Build Report for Job {}".format(self.job.branch)
)
def body(self):
buffer = StringIO()
buffer.write(self.header())
tasks_by_state = collections.defaultdict(dict)
for task_name, task in self.job.tasks.items():
state = task.status().combined_state
tasks_by_state[state][task_name] = task
for state in ('failure', 'error', 'pending', 'success'):
if state in tasks_by_state:
tasks = tasks_by_state[state]
buffer.write('\n')
buffer.write(self.STATUS_HEADERS[state])
buffer.write('\n')
buffer.write(self.listing(tasks))
buffer.write('\n')
return buffer.getvalue()
def email(self):
return self.EMAIL.format(
sender_name=self.sender_name,
sender_email=self.sender_email,
recipient_email=self.recipient_email,
subject=self.subject(),
body=self.body()
)
def show(self, outstream):
outstream.write(self.email())
def send(self, smtp_user, smtp_password, smtp_server, smtp_port):
import smtplib
email = self.email()
server = smtplib.SMTP_SSL(smtp_server, smtp_port)
server.ehlo()
server.login(smtp_user, smtp_password)
server.sendmail(smtp_user, self.recipient_email, email)
server.close()
class CommentReport(Report):
_markdown_badge = '[![{title}]({badge})]({url})'
badges = {
'github': _markdown_badge.format(
title='Github Actions',
url='https://github.com/{repo}/actions?query=branch:{branch}',
badge=(
'https://github.com/{repo}/workflows/Crossbow/'
'badge.svg?branch={branch}'
),
),
'azure': _markdown_badge.format(
title='Azure',
url=(
'https://dev.azure.com/{repo}/_build/latest'
'?definitionId=1&branchName={branch}'
),
badge=(
'https://dev.azure.com/{repo}/_apis/build/status/'
'{repo_dotted}?branchName={branch}'
)
),
'travis': _markdown_badge.format(
title='TravisCI',
url='https://travis-ci.com/{repo}/branches',
badge='https://img.shields.io/travis/{repo}/{branch}.svg'
),
'circle': _markdown_badge.format(
title='CircleCI',
url='https://circleci.com/gh/{repo}/tree/{branch}',
badge=(
'https://img.shields.io/circleci/build/github'
'/{repo}/{branch}.svg'
)
),
'appveyor': _markdown_badge.format(
title='Appveyor',
url='https://ci.appveyor.com/project/{repo}/history',
badge='https://img.shields.io/appveyor/ci/{repo}/{branch}.svg'
),
'drone': _markdown_badge.format(
title='Drone',
url='https://cloud.drone.io/{repo}',
badge='https://img.shields.io/drone/build/{repo}/{branch}.svg'
),
}
def __init__(self, job, crossbow_repo):
self.crossbow_repo = crossbow_repo
super().__init__(job)
def show(self):
url = 'https://github.com/{repo}/branches/all?query={branch}'
sha = self.job.target.head
msg = 'Revision: {}\n\n'.format(sha)
msg += 'Submitted crossbow builds: [{repo} @ {branch}]'
msg += '({})\n'.format(url)
msg += '\n|Task|Status|\n|----|------|'
tasks = sorted(self.job.tasks.items(), key=operator.itemgetter(0))
for key, task in tasks:
branch = task.branch
try:
template = self.badges[task.ci]
badge = template.format(
repo=self.crossbow_repo,
repo_dotted=self.crossbow_repo.replace('/', '.'),
branch=branch
)
except KeyError:
badge = 'unsupported CI service `{}`'.format(task.ci)
msg += '\n|{}|{}|'.format(key, badge)
return msg.format(repo=self.crossbow_repo, branch=self.job.branch)