blob: 93c091e5893300473caa976c8bd5caecca901fba [file] [log] [blame]
/**
* @license
* 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.
*/
import {PluginApi} from '@gerritcodereview/typescript-api/plugin';
import {
ChangeInfo,
RevisionInfo,
} from '@gerritcodereview/typescript-api/rest-api';
import {LitElement, html, css, nothing} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
declare global {
interface HTMLElementTagNameMap {
'zuul-summary-status-tab': ZuulSummaryStatusTab;
}
}
interface ZuulConfig {
enabled?: boolean;
}
interface ZuulJobResult {
job: string;
link?: string;
result: string;
errormsg?: string;
time?: string;
}
interface ZuulTableItem {
succeeded: boolean;
author_name: string;
revision: string | number;
pipeline: string;
rechecks?: number;
gr_date: string;
results: ZuulJobResult[];
}
const ZUUL_PRIORITY = [22348];
@customElement('zuul-summary-status-tab')
export class ZuulSummaryStatusTab extends LitElement {
@property({type: Object})
plugin!: PluginApi;
@property({type: Object}) change?: ChangeInfo;
@property({type: Object}) revision?: RevisionInfo;
@state() private _enabled? = false;
@state() private __table: ZuulTableItem[] = [];
static override get styles() {
return [
css`
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));
}
thead tr th:first-of-type,
tbody tr td:first-of-type {
padding-left: 12px;
}
a:link,
a:visited {
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);
}
tr:hover td {
background-color: var(--hover-background-color, #fffed);
}
.status-SUCCESS {
color: green;
}
.status-FAILURE,
.status-ERROR,
.status-RETRY_LIMIT {
color: red;
}
.status-SKIPPED {
color: #73bcf7;
}
.status-ABORTED,
.status-MERGER_FAILURE,
.status-NODE_FAILURE,
.status-TIMED_OUT,
.status-POST_FAILURE,
.status-CONFIG_ERROR,
.status-DISK_FULL {
color: orange;
}
.date {
color: var(--deemphasized-text-color);
}
`,
];
}
override updated(changed: Map<string, unknown>) {
if (changed.has('change')) this._processChange(this.change);
}
override render() {
if (!this._enabled) {
return html`Zuul integration is not enabled.`;
}
return html`
${this.__table.map(
item => html`
<div style="padding-bottom:2px;">
<table>
<thead>
<tr>
<th>
${item.succeeded
? html`<gr-icon
icon="check"
style="color:var(--success-foreground)"
></gr-icon>`
: html`<gr-icon
icon="close"
style="color:var(--error-foreground)"
></gr-icon>`}
<b>${item.author_name}</b> on Patchset
<b>${item.revision}</b> in pipeline <b>${item.pipeline}</b>
</th>
<th>
${item.rechecks ? html`${item.rechecks} rechecks` : nothing}
</th>
<th class="date">
<gr-date-formatter
show-date-and-time
date-str="${item.gr_date}"
></gr-date-formatter>
</th>
</tr>
</thead>
<tbody>
${item.results.map(
job => html`
<tr>
<td>
${job.link
? html`<a href="${job.link}">${job.job}</a>`
: html`<span
style="color: var(--secondary-text-color)"
>${job.job}</span
>`}
</td>
<td>
<span
class=${`status-${job.result}`}
title=${job.errormsg ?? ''}
>${job.result}</span
>
</td>
<td>${job.time}</td>
</tr>
`,
)}
</tbody>
</table>
</div>
`,
)}
`;
}
private async _processChange(change: any) {
this._enabled = await this._projectEnabled(change.project);
if (this._enabled) this._processMessages(change);
}
private async _projectEnabled(project: string): Promise<boolean | undefined> {
try {
const config = (await this.plugin
.restApi()
.get(
`/projects/${encodeURIComponent(project)}/${encodeURIComponent(
this.plugin.getPluginName(),
)}~config`,
)) as ZuulConfig;
return config?.enabled;
} catch (error) {
console.warn(error);
return false;
}
}
private _match_message_via_tag(msg: any) {
return !!msg.tag?.startsWith('autogenerated:zuul');
}
private _match_message_via_regex(msg: any) {
return /^(.* CI|Zuul)/.test(msg.author.name);
}
private _get_status_and_pipeline(msg: any): [string, string] | false {
const fullMatch = /^Build (\w+) \(([\w]+) pipeline\)\./gm.exec(msg.message);
if (fullMatch) return [fullMatch[1], fullMatch[2]];
const simpleMatch = /^Build (\w+)\./gm.exec(msg.message);
if (simpleMatch) return [simpleMatch[1], 'unknown'];
return false;
}
private _processMessages(change: any) {
const table: any[] = [];
change.messages.forEach((message: any) => {
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 [status, pipeline] = this._get_status_and_pipeline(message) || [];
if (!status) return;
const existingIdx = table.findIndex(
entry =>
entry.author_id === message.author._account_id &&
entry.pipeline === pipeline,
);
let rechecks = 0;
if (existingIdx !== -1 && table[existingIdx].revision === revision) {
rechecks = table[existingIdx].rechecks + 1;
}
const results: any[] = [];
const resultRe =
/^- (?<job>[^ ]+) (?:(?<link>https?:\/\/[^ ]+)|[^ ]+) : ((ERROR (?<errormsg>.*?) in (?<errtime>.*))|(?<result>[^ ]+)( in (?<time>.*))?)/;
message.message.split('\n').forEach((line: string) => {
const match = resultRe.exec(line);
if (match?.groups) {
if (match.groups.result === 'SKIPPED') match.groups.link = '';
if (match.groups.errormsg) {
match.groups.result = 'ERROR';
match.groups.time = match.groups.errtime;
}
results.push(match.groups);
}
});
const row = {
author_name: message.author.name,
author_id: message.author._account_id,
revision,
rechecks,
date,
gr_date: message.date,
status,
succeeded: status === 'succeeded',
pipeline,
results,
};
if (existingIdx === -1) {
table.push(row);
} else {
table[existingIdx] = row;
}
});
this.__table = table.sort((a, b) => {
const pa = ZUUL_PRIORITY.indexOf(a.author_id) >>> 0;
const pb = ZUUL_PRIORITY.indexOf(b.author_id) >>> 0;
return pa - pb || b.date - a.date;
});
}
}