blob: 48e68fdab98b81c1f9575f4401c119c4df37a76a [file] [log] [blame]
// Copyright (c) 2020 Red Hat
//
// Licensed 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.
// TODO(ianw) : find some way to make this configurable
const ZUUL_PRIORITY = [22348];
/*
* Tab contents
*/
class ZuulSummaryStatusTab extends Polymer.Element {
static get properties() {
return {
change: Object,
revision: Object,
};
}
static get template() {
return Polymer.html`
<style>
table {
table-layout: fixed;
width: 100%;
border-collapse: collapse;
}
th, td {
text-align: left;
padding: 2px;
}
th {
background-color: var(--background-color-primary, #f7ffff);
font-weight: normal;
color: var(--primary-text-color, rgb(33, 33, 33));
}
a:link {
color: var(--link-color);
}
tr:nth-child(even) {
background-color: var(--background-color-secondary, #f2f2f2);
}
tr:nth-child(odd) {
background-color: var(--background-color-tertiary, #f7ffff);
}
.status-SUCCESS {
color: green;
}
.status-FAILURE {
color: red;
}
.status-NODE_FAILURE {
color: orange;
}
</style>
<template is="dom-repeat" items="[[__table]]">
<div style="padding-left:5px; padding-bottom:2px;">
<table>
<tr>
<th>
<template is="dom-if" if="{{item.succeeded}}"><span style="color:green"><iron-icon icon="gr-icons:check"></iron-icon></span></template>
<template is="dom-if" if="{{!item.succeeded}}"><span style="color:red"><iron-icon icon="gr-icons:close"></iron-icon></span></template>
<b>[[item.author_name]]</b> on Patchset <b>[[item.revision]]</b> in pipeline <b>[[item.pipeline]]</b></th>
<th><template is="dom-if" if="{{item.rechecks}}">[[item.rechecks]] rechecks</template></th>
<th><b>[[item.date_string]]</b></th>
</tr>
<template is="dom-repeat" items="[[item.results]]" as="job">
<tr>
<template is="dom-if" if="{{job.link}}"><td><a href="{{job.link}}">[[job.job]]</a></td></template>
<template is="dom-if" if="{{!job.link}}"><td><a>[[job.job]]</a></td></template>
<td><span class$="status-[[job.result]]">[[job.result]]</span></td>
<td>[[job.time]]</td>
</tr>
</template>
</table>
</div>
</template>`;
}
_match_message_via_tag(message) {
return (message.tag &&
message.tag.startsWith('autogenerated:zuul')) ? true : false;
}
_match_message_via_regex(message) {
// TODO: allow this to be passed in via config
const authorRe = /^(?<author>.* CI|Zuul)/;
const author = authorRe.exec(message.author.name);
return author ? true : false;
}
_get_status_and_pipeline(message) {
// Look for the full Zuul-3ish build status message, e.g.:
// Build succeeded (check pipeline).
const statusRe = /^Build (?<status>\w+) \((?<pipeline>[\w]+) pipeline\)\./gm;
let statusMatch = statusRe.exec(message.message);
if (!statusMatch) {
// Match non-pipeline CI comments, e.g.:
// Build succeeded.
const statusRe = /^Build (?<status>\w+)\./gm;
statusMatch = statusRe.exec(message.message);
}
if (!statusMatch) {
return false; // we can't parse this
}
const status = statusMatch.groups.status;
const pipeline = statusMatch.groups.pipeline ?
statusMatch.groups.pipeline : 'unknown';
return [status, pipeline];
}
ready() {
super.ready();
/*
* change-view-tab-content gets passed ChangeInfo object [1],
* registered in the property "change". We walk the list of
* messages with some regexps to extract into a datastructure
* stored in __table
*
* __table is an [] of objects
*
* author: "<string> CI"
* date: date message posted
* date_string: printable version of date
* revision: the revision the patchset was made against
* rechecks: the number of times we've seen the same
* ci run for the same revision
* status: one of <succeeded|failed>
* pipeline: string of reporting pipeline
* (may be undefined for some CI)
* results: [] of objects
* job: job name
* link: raw URL link to logs
* result: one of <SUCCESS|FAILURE>
* time: duration of run in human string (e.g. 2m 5s)
*
* This is then presented by the template
*
* [1] https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
*/
this.__table = [];
this.change.messages.forEach(message => {
if (! (this._match_message_via_tag(message) ||
this._match_message_via_regex(message))) {
return;
}
const date = new Date(message.date);
const revision = message._revision_number;
const sp = this._get_status_and_pipeline(message);
if (!sp) {
// This shouldn't happen as we've validated it is a Zuul message.
return;
}
const status = sp[0];
const pipeline = sp[1];
// We only want the latest entry for each CI system in
// each pipeline
const existing = this.__table.findIndex(entry =>
(entry.author_id === message.author._account_id) &&
(entry.pipeline === pipeline));
// If this is a comment by the same CI on the same pipeline and
// the same revision, it's considered a "recheck" ... i.e. likely
// manually triggered to run again. Take a note of this.
let rechecks = 0;
if (existing != -1) {
if (this.__table[existing].revision === revision) {
rechecks = this.__table[existing].rechecks + 1;
}
}
// Find each result line, e.g. :
// - openstack-tox-py35 http://... : SUCCESS in 2m 45
const results = [];
const lines = message.message.split('\n');
const resultRe = /^- (?<job>[^ ]+) (?:(?<link>https?:\/\/[^ ]+)|[^ ]+) : (?<result>[^ ]+) in (?<time>.*)/;
lines.forEach(line => {
const result = resultRe.exec(line);
if (result) {
results.push(result.groups);
}
});
const table = {
author_name: message.author.name,
author_id: message.author._account_id,
revision,
rechecks,
date,
date_string: date.toLocaleString(),
status,
succeeded: status === 'succeeded' ? true : false,
pipeline,
results,
};
if (existing == -1) {
this.__table.push(table);
} else {
this.__table[existing] = table;
}
// Sort first by listed priority, then by date
this.__table.sort((a, b) => {
// >>> 0 is just a trick to convert -1 to uint max
// of 2^32-1
const p_a = ZUUL_PRIORITY.indexOf(a.author_id) >>> 0;
const p_b = ZUUL_PRIORITY.indexOf(b.author_id) >>> 0;
const priority = p_a - p_b;
const date = b.date - a.date;
return priority || date;
});
});
}
}
customElements.define('zuul-summary-status-tab',
ZuulSummaryStatusTab);
/*
* Tab Header Element
*/
class ZuulSummaryStatusTabHeader extends Polymer.Element {
static get template() {
return Polymer.html`Zuul Summary`;
}
}
customElements.define('zuul-summary-status-tab-header',
ZuulSummaryStatusTabHeader);
/*
* Install plugin
*/
Gerrit.install(plugin => {
'use strict';
plugin.registerDynamicCustomComponent(
'change-view-tab-header',
'zuul-summary-status-tab-header'
);
plugin.registerDynamicCustomComponent(
'change-view-tab-content',
'zuul-summary-status-tab'
);
});