Convert from Polymer to Lit
Change-Id: Ib9c32a740d90aaeaa0d5be4150e66834e358bf05
diff --git a/.gitignore b/.gitignore
index 0f55ddc..53acbd5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-
+.DS_Store
/.classpath
/.primary_build_tool
/.project
diff --git a/web/.eslintrc.js b/web/.eslintrc.js
deleted file mode 100644
index cdb0d00..0000000
--- a/web/.eslintrc.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * @license
- * Copyright (C) 2021 The Android Open Source Project
- *
- * 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.
- */
-__plugindir = 'zuul-results-summary/web';
-module.exports = {
- extends: '../../.eslintrc.js',
-};
diff --git a/web/BUILD b/web/BUILD
index 818e7a7..4ee1836 100644
--- a/web/BUILD
+++ b/web/BUILD
@@ -1,5 +1,6 @@
load("//tools/js:eslint.bzl", "plugin_eslint")
load("//tools/bzl:js.bzl", "gerrit_js_bundle")
+load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
package_group(
name = "visibility",
@@ -8,9 +9,34 @@
package(default_visibility = [":visibility"])
+ts_config(
+ name = "tsconfig",
+ src = "tsconfig.json",
+ deps = [
+ "//plugins:tsconfig-plugins-base.json",
+ ],
+)
+
+ts_project(
+ name = "zuul-results-summary-ts",
+ srcs = glob(
+ ["**/*.ts"],
+ exclude = ["**/*test*"],
+ ),
+ incremental = True,
+ out_dir = "_bazel_ts_out",
+ tsc = "//tools/node_tools:tsc-bin",
+ tsconfig = ":tsconfig",
+ deps = [
+ "@plugins_npm//@gerritcodereview/typescript-api",
+ "@plugins_npm//lit",
+ ],
+)
+
gerrit_js_bundle(
name = "zuul-results-summary",
- entry_point = "plugin.js",
+ srcs = [":zuul-results-summary-ts"],
+ entry_point = "_bazel_ts_out/plugin.js",
)
# bazel run plugins/zuul-results-summary/web:lint_bin
diff --git a/web/eslint.config.js b/web/eslint.config.js
new file mode 100644
index 0000000..74ce3bf
--- /dev/null
+++ b/web/eslint.config.js
@@ -0,0 +1,18 @@
+/**
+ * @license
+ * Copyright 2021 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+const {defineConfig} = require('eslint/config');
+
+// eslint-disable-next-line no-undef
+__plugindir = 'zuul-results-summary/web';
+
+const gerritEslint = require('../../eslint.config.js');
+
+module.exports = defineConfig([
+ {
+ extends: [gerritEslint],
+ },
+]);
\ No newline at end of file
diff --git a/web/plugin.js b/web/plugin.js
deleted file mode 100644
index 3a9907f..0000000
--- a/web/plugin.js
+++ /dev/null
@@ -1,411 +0,0 @@
-// 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 {
-
- /** Get properties
- *
- * @returns {dict} change and revision
- */
- static get properties() {
- return {
- plugin: Object,
- _enabled: Boolean,
- change: {
- type: Object,
- observer: '_processChange',
- },
- revision: Object,
- };
- }
-
- /** Get template
- *
- * @returns {Polymer.html} the template
- */
- 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));
- }
-
- 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 {
- color: red;
- }
-
- .status-ERROR {
- color: red;
- }
-
- .status-RETRY_LIMIT {
- color: red;
- }
-
- .status-SKIPPED {
- color: #73bcf7;
- }
-
- .status-ABORTED {
- color: orange;
- }
-
- .status-MERGER_FAILURE {
- color: orange;
- }
-
- .status-NODE_FAILURE {
- color: orange;
- }
-
- .status-TIMED_OUT {
- color: orange;
- }
-
- .status-POST_FAILURE {
- color: orange;
- }
-
- .status-CONFIG_ERROR {
- color: orange;
- }
-
- .status-DISK_FULL {
- color: orange;
- }
-
- .date {
- color: var(--deemphasized-text-color);
- }
- </style>
-
- <template is="dom-if" if="[[!_enabled]]">
- Zuul integration is not enabled.
- </template>
- <template is="dom-repeat" items="[[__table]]">
- <div style="padding-bottom:2px;">
- <table>
- <thead>
- <tr>
- <th>
- <template is="dom-if" if="{{item.succeeded}}"><gr-icon icon="check" style="color:var(--success-foreground)"></gr-icon></template>
- <template is="dom-if" if="{{!item.succeeded}}"><gr-icon icon="close" style="color:var(--error-foreground)"></gr-icon></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><span class="date"><gr-date-formatter show-date-and-time="" date-str="[[item.gr_date]]"></gr-date-formatter></span></th>
- </tr>
- </thead>
- <tbody>
- <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><span style="color: var(--secondary-text-color)">[[job.job]]</span></td></template>
- <template is="dom-if" if="{{job.errormsg}}"><td><span title="[[job.errormsg]]" class$="status-[[job.result]]">[[job.result]]</span></td></template>
- <template is="dom-if" if="{{!job.errormsg}}"><td><span class$="status-[[job.result]]">[[job.result]]</span></td></template>
- <td>[[job.time]]</td>
- </tr>
- </template>
- </tbody>
- </table>
- </div>
- </template>`;
- }
-
- /**
- * Process the change. Retrieve project configuration, and if it's
- * enabled for Zuul, parse the messages and render Zuul Summary tab.
- *
- * @param {Object} change
- */
- async _processChange(change) {
- // TODO(davido): Cache results of project config request
- this._enabled = await this._projectEnabled(change.project);
- if (this._enabled) {
- this._processMessages(change);
- }
- }
-
- /**
- * Returns whether the project is enabled for Zuul.
- *
- * @param {string} project
- * @return {promise<boolean>} Resolves to true if the project is enabled
- * otherwise, false.
- */
- async _projectEnabled(project) {
- const configPromise = this.plugin.restApi().get(
- `/projects/${encodeURIComponent(project)}/` +
- `${encodeURIComponent(this.plugin.getPluginName())}~config`);
- try {
- const config = await configPromise;
- return config && config.enabled;
- } catch (error) {
- console.log(error);
- return false;
- }
- }
-
- /** Look for Zuul tag in message
- *
- * @param{ChangeMessageInfo} message
- * @returns {bool} if this is a Zuul message or not
- */
- _match_message_via_tag(message) {
- return !!(message.tag &&
- message.tag.startsWith('autogenerated:zuul'));
- }
-
- /** Look for 3rd-party CI messages via regex
- *
- * @param{ChangeMessageInfo} message
- * @returns {bool} if this is a Zuul-ish message or not
- */
- _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;
- }
-
- /** Extract the status and pipeline from the message
- *
- * @param{ChangeMessageInfo} message
- * @returns {list} status and pipeline
- */
- _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];
- }
-
- /** Change Modified */
- _processMessages(change) {
- /*
- * 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 data structure
- * stored in __table
- *
- * __table is an [] of objects
- *
- * author: "<string> CI"
- * date: Date object of date message posted, useful for
- * sorting, diffs, etc.
- * gr_date: original message timestamp sutiable to pass to
- * gr-date-formatter
- * 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 = [];
- 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
- const results = [];
- const lines = message.message.split('\n');
- // We have to match a few different things ...
- // A "standard" line is like
- // - passing-job http://... : SUCCESS in 2m 45s
- // Skipped jobs don't have a time, e.g.
- // - skipped-job http://... : SKIPPED
- // Error status has a string before the time
- // - error-job http://... : ERROR A freeform string in 2m 45s
-
- const resultRe = /^- (?<job>[^ ]+) (?:(?<link>https?:\/\/[^ ]+)|[^ ]+) : ((ERROR (?<errormsg>.*?) in (?<errtime>.*))|(?<result>[^ ]+)( in (?<time>.*))?)/;
- lines.forEach(line => {
- const result = resultRe.exec(line);
- if (result) {
- if (result.groups.result === "SKIPPED") {
- result.groups.link = null;
- }
- // Note you can't duplicate match group names, even if
- // it's behind an | statement like above. So for error
- // matches we copy things into the right place to display.
- if (result.groups.errormsg) {
- result.groups.result = "ERROR";
- result.groups.time = result.groups.errtime;
- }
- results.push(result.groups);
- }
- });
-
- const table = {
- 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 (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 {
- /** Get template
- *
- * @returns {Polymer.html} the template
- */
- 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'
- );
-});
diff --git a/web/plugin.ts b/web/plugin.ts
new file mode 100644
index 0000000..d696391
--- /dev/null
+++ b/web/plugin.ts
@@ -0,0 +1,32 @@
+/**
+ * @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 '@gerritcodereview/typescript-api/gerrit';
+import './zuul-summary-status-tab';
+import './zuul-summary-status-tab-header';
+
+window.Gerrit?.install(plugin => {
+ plugin.registerDynamicCustomComponent(
+ 'change-view-tab-header',
+ 'zuul-summary-status-tab-header',
+ );
+
+ plugin.registerDynamicCustomComponent(
+ 'change-view-tab-content',
+ 'zuul-summary-status-tab',
+ );
+});
diff --git a/web/tsconfig.json b/web/tsconfig.json
index 19040ee..2e886b5 100644
--- a/web/tsconfig.json
+++ b/web/tsconfig.json
@@ -1,5 +1,8 @@
{
"extends": "../../tsconfig-plugins-base.json",
+ "compilerOptions": {
+ "outDir": "../../../.ts-out/plugins/zuul-results-summary" /* overridden by bazel */
+ },
"include": [
"**/*.ts"
]
diff --git a/web/zuul-summary-status-tab-header.ts b/web/zuul-summary-status-tab-header.ts
new file mode 100644
index 0000000..b60b1c7
--- /dev/null
+++ b/web/zuul-summary-status-tab-header.ts
@@ -0,0 +1,32 @@
+/**
+ * @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 {LitElement, html} from 'lit';
+import {customElement} from 'lit/decorators.js';
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'zuul-summary-status-tab-header': ZuulSummaryStatusTabHeader;
+ }
+}
+
+@customElement('zuul-summary-status-tab-header')
+export class ZuulSummaryStatusTabHeader extends LitElement {
+ override render() {
+ return html`Zuul Summary`;
+ }
+}
diff --git a/web/zuul-summary-status-tab.ts b/web/zuul-summary-status-tab.ts
new file mode 100644
index 0000000..93c091e
--- /dev/null
+++ b/web/zuul-summary-status-tab.ts
@@ -0,0 +1,310 @@
+/**
+ * @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;
+ });
+ }
+}