| /** |
| * @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; |
| }); |
| } |
| } |