Add eslint rules and do a run
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/.eslintignore
diff --git a/.eslintrc.json b/.eslintrc.json
index 61dd1c8..8adc172 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -4,7 +4,7 @@
         "google"
     ],
     "parserOptions": {
-        "ecmaVersion": 8,
+        "ecmaVersion": 2018,
         "sourceType": "module"
     },
     "env": {
@@ -235,4 +235,4 @@
             }
         }
     ]
-}
\ No newline at end of file
+}
diff --git a/BUILD b/BUILD
index 80fb03d..7d3467f 100644
--- a/BUILD
+++ b/BUILD
@@ -1,5 +1,6 @@
 load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
 load("//tools/bzl:js.bzl", "bundle_assets", "polygerrit_plugin")
+load("//tools/js:eslint.bzl", "eslint")
 
 polygerrit_plugin(
     name = "zuul_results_summary",
@@ -19,3 +20,24 @@
       "@tools_npm//rollup-plugin-node-resolve",
     ],
 )
+
+# Define the eslinter for the plugin
+# The eslint macro creates 2 rules: lint_test and lint_bin
+eslint(
+    name = "lint",
+    srcs = glob([
+        "zuul-results-summary/**/*.js",
+    ]),
+    config = ".eslintrc.json",
+    data = [],
+    extensions = [
+        ".js",
+    ],
+    ignore = ".eslintignore",
+    plugins = [
+        "@npm//eslint-config-google",
+        "@npm//eslint-plugin-html",
+        "@npm//eslint-plugin-import",
+        "@npm//eslint-plugin-jsdoc",
+    ],
+)
diff --git a/zuul-results-summary/zuul-results-summary.js b/zuul-results-summary/zuul-results-summary.js
index 1bf9de4..5de9e0a 100644
--- a/zuul-results-summary/zuul-results-summary.js
+++ b/zuul-results-summary/zuul-results-summary.js
@@ -12,22 +12,21 @@
 // License for the specific language governing permissions and limitations
 // under the License.
 
-let ZUUL_PRIORITY = [ 22348 ];
+const ZUUL_PRIORITY = [22348];
 
 /*
  * Tab contents
  */
 class ZuulSummaryStatusTab extends Polymer.Element {
+  static get properties() {
+    return {
+      change: Object,
+      revision: Object,
+    };
+  }
 
-    static get properties() {
-        return {
-            change: Object,
-            revision: Object
-        }
-    }
-
-    static get template() {
-        return Polymer.html`
+  static get template() {
+    return Polymer.html`
   <style>
     table {
       table-layout: fixed;
@@ -92,179 +91,180 @@
    </table>
    </div>
   </template>`;
-    }
+  }
 
-    _match_message_via_tag(message) {
-        return (message.tag &&
+  _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
-        let authorRe = /^(?<author>.* CI|Zuul)/;
-        let author = authorRe.exec(message.author.name);
-        return author ? 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.:
+  _get_status_and_pipeline(message) {
+    /* Look for the full Zuul-3ish build status message, e.g.:
          *   Build succeeded (check pipeline).
          */
-        let statusRe = /^Build (?<status>\w+) \((?<pipeline>[\w]+) pipeline\)\./gm
-        let statusMatch = statusRe.exec(message.message);
-        if (!statusMatch) {
-            /* Match non-pipeline CI comments, e.g.:
+    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.
              */
-            let statusRe = /^Build (?<status>\w+)\./gm
-            statusMatch = statusRe.exec(message.message)
-        }
-        if (!statusMatch) {
-            return false; // we can't parse this
-        }
-
-        let status = statusMatch.groups.status
-        let pipeline = statusMatch.groups.pipeline ?
-            statusMatch.groups.pipeline : 'unknown';
-        return [status, pipeline]
+      const statusRe = /^Build (?<status>\w+)\./gm;
+      statusMatch = statusRe.exec(message.message);
+    }
+    if (!statusMatch) {
+      return false; // we can't parse this
     }
 
-    ready() {
-        super.ready();
+    const status = statusMatch.groups.status;
+    const pipeline = statusMatch.groups.pipeline ?
+      statusMatch.groups.pipeline : 'unknown';
+    return [status, pipeline];
+  }
 
-        /*
-         * 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
-         *  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) => {
+  ready() {
+    super.ready();
 
-            if  (! (this._match_message_via_tag(message) ||
+    /*
+     * 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;
-            }
+        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];
+      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) &&
+      // 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;
-                }
-            }
+      // 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
-             */
-            let results = [];
-            const lines = message.message.split("\n");
-            const resultRe = /^- (?<job>[^ ]+) (?<link>[^ ]+) : (?<result>[^ ]+) in (?<time>.*)/
-            lines.forEach((line) => {
-                const result = resultRe.exec(line);
-                if (result) {
-                    results.push(result.groups);
-                }
-            });
+      // 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>[^ ]+) : (?<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': revision,
-                'rechecks': rechecks,
-                'date': date,
-                'date_string': date.toLocaleString(),
-                'status': status,
-                'succeeded': status === 'succeeded' ? true : false,
-                'pipeline': pipeline,
-                'results': results
-            }
+      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;
-            }
+      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
-                let p_a = ZUUL_PRIORITY.indexOf(a.author_id) >>> 0;
-                let p_b = ZUUL_PRIORITY.indexOf(b.author_id) >>> 0;
-                let priority = p_a - p_b;
-                let date = b.date - a.date;
-                return priority || date;
-            });
-        });
-    }
+      // 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);
+    ZuulSummaryStatusTab);
 
 /*
  * Tab Header Element
  */
 class ZuulSummaryStatusTabHeader extends Polymer.Element {
-   static get template() {
-     return Polymer.html`Zuul Summary`;
-   }
- }
+  static get template() {
+    return Polymer.html`Zuul Summary`;
+  }
+}
 
 customElements.define('zuul-summary-status-tab-header',
-                      ZuulSummaryStatusTabHeader);
-
+    ZuulSummaryStatusTabHeader);
 
 /*
  * Install plugin
  */
 
 Gerrit.install(plugin => {
-    'use strict';
-    
-    plugin.registerDynamicCustomComponent(
-        'change-view-tab-header',
-        'zuul-summary-status-tab-header'
-    );
+  'use strict';
 
-    plugin.registerDynamicCustomComponent(
-        'change-view-tab-content',
-        'zuul-summary-status-tab'
-    );
+  plugin.registerDynamicCustomComponent(
+      'change-view-tab-header',
+      'zuul-summary-status-tab-header'
+  );
+
+  plugin.registerDynamicCustomComponent(
+      'change-view-tab-content',
+      'zuul-summary-status-tab'
+  );
 });