Merge branch 'stable-3.2' into stable-3.3

* stable-3.2: (38 commits)
  docker: make sure to cleanup containers before force exit
  fixup! Add TaskTree definitions more directly
  fixup! refactor predicate cache into its own class
  fixup! Add support for tasks-factory and names-factory keywords
  fixup! Support outputting elapsed evaluation time on tasks
  fixup! Revert "Revert "plugin:task Adds support for names-factory of type change""
  Add a Container.toString() to help when debugging.
  Harden inputs to basename
  Add Zuul postflight publish job
  Pass around Task parent instead of its fields
  Fix to alter change context for TaskChangeFactories
  Add tasks-factories static properties tests
  Add a names-factory STATIC Properties test
  Add ensures to the task tests setups
  Split the task preload properties test out
  Split the task preload optional test out
  Split the append preloaded subtasks test out
  Split the task override preload pass/fail tests out
  Split the task preload hints tests out
  Split the task preload preload test out
  ...

Change-Id: Ia601c9be023966ff6d5d7f74e1545302056c97ec
diff --git a/.zuul.yaml b/.zuul.yaml
index ac08784..27c8491 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -5,7 +5,7 @@
         tools/playbooks/install_docker.yaml
     vars:
         bazelisk_test_targets: "plugins/task/lint_test plugins/task/..."
-        
+
 - project:
     description: |
       Build the plugin in check, and also build and publish it after
diff --git a/BUILD b/BUILD
index 56452dc..0b04753 100644
--- a/BUILD
+++ b/BUILD
@@ -1,12 +1,6 @@
-load(
-    "//tools/bzl:plugin.bzl",
-    "PLUGIN_DEPS",
-    "gerrit_plugin",
-)
-load("//tools/bzl:genrule2.bzl", "genrule2")
-load("//tools/bzl:js.bzl", "polygerrit_plugin")
+load("//tools/bzl:plugin.bzl", "gerrit_plugin")
+load("//tools/bzl:js.bzl", "gerrit_js_bundle")
 load("//tools/js:eslint.bzl", "eslint")
-
 plugin_name = "task"
 
 gerrit_plugin(
@@ -18,30 +12,15 @@
         "Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/" + plugin_name,
         "Gerrit-Module: com.googlesource.gerrit.plugins.task.Modules$Module",
     ],
-    resource_jars = [":gr-task-plugin-static"],
+    resource_jars = [":gr-task-plugin"],
     resources = glob(["src/main/resources/**/*"]),
     javacopts = [ "-Werror", "-Xlint:all", "-Xlint:-classfile", "-Xlint:-processing"],
 )
 
-genrule2(
-    name = "gr-task-plugin-static",
-    srcs = [":gr-task-plugin"],
-    outs = ["gr-task-plugin-static.jar"],
-    cmd = " && ".join([
-        "mkdir $$TMP/static",
-        "cp -r $(locations :gr-task-plugin) $$TMP/static",
-        "cd $$TMP",
-        "zip -Drq $$ROOT/$@ -g .",
-    ]),
-)
-
-polygerrit_plugin(
+gerrit_js_bundle(
     name = "gr-task-plugin",
-    srcs = glob([
-        "gr-task-plugin/*.html",
-        "gr-task-plugin/*.js",
-    ]),
-    app = "plugin.html",
+    srcs = glob(["gr-task-plugin/*.js"]),
+    entry_point = "gr-task-plugin/plugin.js",
 )
 
 sh_test(
diff --git a/WORKSPACE b/WORKSPACE
index af57aa6..f573f76 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -8,7 +8,7 @@
 load("//:bazlets.bzl", "load_bazlets")
 
 load_bazlets(
-    commit = "1dd03e38f46e56defc91b6ab4a4d9879f6083de1",
+    commit = "b4324e30289c2f6a2a07a4f0a9df6b1fce85ef1a",
     #local_path = "/home/<user>/projects/bazlets",
 )
 
@@ -29,26 +29,6 @@
     yarn_lock = "//:yarn.lock",
 )
 
-# Load closure compiler with transitive dependencies
-load("@io_bazel_rules_closure//closure:repositories.bzl", "rules_closure_dependencies", "rules_closure_toolchains")
-
-rules_closure_dependencies()
-
-rules_closure_toolchains()
-
-# Load Gerrit npm_binary toolchain
-load("@com_googlesource_gerrit_bazlets//tools:js.bzl", "GERRIT", "npm_binary")
-
-npm_binary(
-    name = "polymer-bundler",
-    repository = GERRIT,
-)
-
-npm_binary(
-    name = "crisper",
-    repository = GERRIT,
-)
-
 # Load plugin API
 load(
     "@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
diff --git a/gr-task-plugin/gr-task-plugin-tasks.js b/gr-task-plugin/gr-task-plugin-tasks.js
new file mode 100644
index 0000000..54585d1
--- /dev/null
+++ b/gr-task-plugin/gr-task-plugin-tasks.js
@@ -0,0 +1,49 @@
+/**
+ * @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.
+ */
+
+import {htmlTemplate} from './gr-task-plugin-tasks_html.js';
+
+class GrTaskPluginTasks extends Polymer.Element {
+  static get is() {
+    return 'gr-task-plugin-tasks';
+  }
+
+  static get template() {
+    return htmlTemplate;
+  }
+
+  static get properties() {
+    return {
+      tasks: {
+        type: Array,
+        notify: true,
+        value() { return []; },
+      },
+
+      show_all: {
+        type: String,
+        notify: true,
+      },
+    };
+  }
+
+  _can_show(show, task) {
+    return show === 'true' || task.showOnFilter;
+  }
+}
+
+customElements.define(GrTaskPluginTasks.is, GrTaskPluginTasks);
diff --git a/gr-task-plugin/gr-task-plugin-tasks_html.js b/gr-task-plugin/gr-task-plugin-tasks_html.js
new file mode 100644
index 0000000..df1644f
--- /dev/null
+++ b/gr-task-plugin/gr-task-plugin-tasks_html.js
@@ -0,0 +1,58 @@
+/**
+ * @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.
+ */
+
+export const htmlTemplate = Polymer.html`
+<template is="dom-repeat" as="task" items="[[tasks]]">
+  <template is="dom-if" if="[[_can_show(show_all, task)]]">
+    <li>
+      <style>
+        /* Matching colors with core code. */
+        .green {
+          color: #9fcc6b;
+        }
+        .red {
+          color: #FFA62F;
+        }
+      </style>
+      <template is="dom-if" if="[[task.icon.id]]">
+        <gr-tooltip-content
+            has-tooltip
+            title="In Progress">
+            <iron-icon
+              icon="gr-icons:hourglass"
+              class="green"
+              hidden$="[[!task.in_progress]]">
+            </iron-icon>
+        </gr-tooltip-content>
+        <gr-tooltip-content
+            has-tooltip
+            title$="[[task.icon.tooltip]]">
+            <iron-icon
+              icon="[[task.icon.id]]"
+              class$="[[task.icon.color]]">
+            </iron-icon>
+        </gr-tooltip-content>
+      </template>
+      [[task.message]]
+    </li>
+  </template>
+  <ul style="list-style-type:none; margin: 0 0 0 0; padding: 0 0 0 2em;">
+    <gr-task-plugin-tasks
+        tasks="[[task.sub_tasks]]"
+        show_all$="[[show_all]]"> </gr-task-plugin-tasks>
+  </ul>
+</template>`;
diff --git a/gr-task-plugin/gr-task-plugin.html b/gr-task-plugin/gr-task-plugin.html
deleted file mode 100644
index c6c3746..0000000
--- a/gr-task-plugin/gr-task-plugin.html
+++ /dev/null
@@ -1,146 +0,0 @@
-<!--
-Copyright (C) 2019 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.
--->
-
-<dom-module id="gr-task-plugin">
-  <template>
-      <style>
-        ul {
-          padding-left: 0.5em;
-          margin-top: 0;
-        }
-        h3 { padding-left: 0.1em; }
-        .cursor { cursor: pointer; }
-        #tasks_header {
-          align-items: center;
-          background-color: #fafafa;
-          border-top: 1px solid #ddd;
-          display: flex;
-          padding: 6px 1rem;
-        }
-        .links {
-          color: blue;
-          cursor: pointer;
-          text-decoration: underline;
-        }
-        .no-margins { margin: 0 0 0 0; }
-      </style>
-
-      <div id="tasks" hidden$="[[!_tasks.length]]">
-        <div id="tasks_header" style="display: flex;">
-          <iron-icon
-              icon="gr-icons:expand-less"
-              hidden$="[[!_expand_all]]"
-              on-tap="_switch_expand"
-              class="cursor"> </iron-icon>
-          <iron-icon
-              icon="gr-icons:expand-more"
-              hidden$="[[_expand_all]]"
-              on-tap="_switch_expand"
-              class="cursor"> </iron-icon>
-          <div style="display: flex; align-items: center; column-gap: 1em;">
-          <h3 class="no-margins" on-tap="_switch_expand" class="cursor"> Tasks </h3>
-          <template is="dom-if" if="[[_is_show_all(_show_all)]]">
-            <p class="no-margins">All ([[_all_count]]) |&nbsp;
-              <span
-                  on-click="_needs_and_blocked_tap"
-                  class="links">Needs ([[_ready_count]]) + Blocked ([[_fail_count]])</span>
-            <p>
-          </template>
-          <template is="dom-if" if="[[!_is_show_all(_show_all)]]">
-            <p class="no-margins"> <span
-                  class="links"
-                  on-click="_show_all_tap">All ([[_all_count]])</span>
-              &nbsp;| Needs ([[_ready_count]]) + Blocked ([[_fail_count]])</p>
-          </template>
-        </div>
-        </div>
-        <div hidden$="[[!_expand_all]]">
-          <ul style="list-style-type:none;">
-            <gr-task-plugin-tasks
-                tasks="[[_tasks]]"
-                show_all$="[[_show_all]]"> </gr-task-plugin-tasks>
-          </ul>
-        </div>
-      </div>
-  </template>
-  <script src="gr-task-plugin.js"></script>
-</dom-module>
-
-<dom-module id="gr-task-plugin-tasks">
-  <template>
-    <template is="dom-repeat" as="task" items="[[tasks]]">
-      <template is="dom-if" if="[[_can_show(show_all, task)]]">
-        <li style="padding: 0.2em;">
-          <style>
-            /* Matching colors with core code. */
-            .green {
-              color: #9fcc6b;
-            }
-            .red {
-              color: #FFA62F;
-            }
-          </style>
-          <template is="dom-if" if="[[task.icon.id]]">
-            <gr-tooltip-content
-                has-tooltip
-                title="In Progress">
-                <iron-icon
-                  icon="gr-icons:hourglass"
-                  class="green"
-                  hidden$="[[!task.in_progress]]">
-                </iron-icon>
-            </gr-tooltip-content>
-            <gr-tooltip-content
-                has-tooltip
-                title$="[[task.icon.tooltip]]">
-                <iron-icon
-                  icon="[[task.icon.id]]"
-                  class$="[[task.icon.color]]">
-                </iron-icon>
-            </gr-tooltip-content>
-          </template>
-          [[task.message]]
-        </li>
-      </template>
-      <ul style="list-style-type:none; margin: 0 0 0 0; padding: 0 0 0 2em;">
-      <gr-task-plugin-tasks
-          tasks="[[task.sub_tasks]]"
-          show_all$="[[show_all]]"> </gr-task-plugin-tasks>
-      </ul>
-    </template>
-  </template>
-  <script>
-      Polymer({
-        is: 'gr-task-plugin-tasks',
-        properties: {
-          tasks: {
-            type: Array,
-            notify: true,
-            value() { return []; },
-          },
-
-          show_all: {
-            type: String,
-            notify: true,
-          },
-        },
-
-        _can_show(show, task) {
-          return show === 'true' || task.showOnFilter;
-        },
-      });
-  </script>
-</dom-module>
diff --git a/gr-task-plugin/gr-task-plugin.js b/gr-task-plugin/gr-task-plugin.js
index ace9f60..34fa467 100644
--- a/gr-task-plugin/gr-task-plugin.js
+++ b/gr-task-plugin/gr-task-plugin.js
@@ -1,6 +1,6 @@
 /**
  * @license
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -14,24 +14,34 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function() {
-  'use strict';
 
-  const Defs = {};
-  /**
-   * @typedef {{
-   *  message: string,
-   *  sub_tasks: Array<Defs.Task>,
-   *  hint: ?string,
-   *  name: string,
-   *  status: string
-   * }}
-   */
-  Defs.Task;
+import './gr-task-plugin-tasks.js';
 
-  Polymer({
-    is: 'gr-task-plugin',
-    properties: {
+import {htmlTemplate} from './gr-task-plugin_html.js';
+
+const Defs = {};
+/**
+ * @typedef {{
+ *  message: string,
+ *  sub_tasks: Array<Defs.Task>,
+ *  hint: ?string,
+ *  name: string,
+ *  status: string
+ * }}
+ */
+Defs.Task;
+
+class GrTaskPlugin extends Polymer.Element {
+  static get is() {
+    return 'gr-task-plugin';
+  }
+
+  static get template() {
+    return htmlTemplate;
+  }
+
+  static get properties() {
+    return {
       change: {
         type: Object,
       },
@@ -72,119 +82,127 @@
         notify: true,
         value: 0,
       },
-    },
+    };
+  }
 
-    _is_show_all(show_all) {
-      return show_all === 'true';
-    },
+  _is_show_all(show_all) {
+    return show_all === 'true';
+  }
 
-    attached() {
-      this._getTasks();
-    },
+  connectedCallback() {
+    super.connectedCallback();
 
-    _getTasks() {
-      const endpoint =
-          `/changes/?q=change:${this.change._number}&--task--applicable`;
+    this._getTasks();
+  }
 
-      return this.plugin.restApi().get(endpoint).then(response => {
-        if (response && response.length === 1) {
-          const cinfo = response[0];
-          if (cinfo.plugins) {
-            const taskPluginInfo = cinfo.plugins.find(
-                pluginInfo => pluginInfo.name === 'task');
+  _getTasks() {
+    if (!this.change) {
+      return;
+    }
 
-            if (taskPluginInfo) {
-              this._tasks = this._addTasks(taskPluginInfo.roots);
-            }
+    const endpoint =
+        `/changes/?q=change:${this.change._number}&--task--applicable`;
+
+    return this.plugin.restApi().get(endpoint).then(response => {
+      if (response && response.length === 1) {
+        const cinfo = response[0];
+        if (cinfo.plugins) {
+          const taskPluginInfo = cinfo.plugins.find(
+              pluginInfo => pluginInfo.name === 'task');
+
+          if (taskPluginInfo) {
+            this._tasks = this._addTasks(taskPluginInfo.roots);
           }
         }
-      });
-    },
-
-    _computeIcon(task) {
-      const icon = {};
-      switch (task.status) {
-        case 'FAIL':
-          icon.id = 'gr-icons:close';
-          icon.color = 'red';
-          icon.tooltip = 'Failed';
-          break;
-        case 'READY':
-          icon.id = 'gr-icons:rebase';
-          icon.color = 'green';
-          icon.tooltip = 'Ready';
-          break;
-        case 'INVALID':
-          icon.id = 'gr-icons:abandon';
-          icon.color = 'red';
-          icon.tooltip = 'Invalid';
-          break;
-        case 'WAITING':
-          icon.id = 'gr-icons:side-by-side';
-          icon.color = 'red';
-          icon.tooltip = 'Waiting';
-          break;
-        case 'PASS':
-          icon.id = 'gr-icons:check';
-          icon.color = 'green';
-          icon.tooltip = 'Passed';
-          break;
       }
-      return icon;
-    },
+    });
+  }
 
-    _isFailOrReadyOrInvalid(task) {
-      switch (task.status) {
-        case 'FAIL':
-        case 'READY':
-        case 'INVALID':
-          return true;
-      }
-      return false;
-    },
+  _computeIcon(task) {
+    const icon = {};
+    switch (task.status) {
+      case 'FAIL':
+        icon.id = 'gr-icons:close';
+        icon.color = 'red';
+        icon.tooltip = 'Failed';
+        break;
+      case 'READY':
+        icon.id = 'gr-icons:rebase';
+        icon.color = 'green';
+        icon.tooltip = 'Ready';
+        break;
+      case 'INVALID':
+        icon.id = 'gr-icons:abandon';
+        icon.color = 'red';
+        icon.tooltip = 'Invalid';
+        break;
+      case 'WAITING':
+        icon.id = 'gr-icons:side-by-side';
+        icon.color = 'red';
+        icon.tooltip = 'Waiting';
+        break;
+      case 'PASS':
+        icon.id = 'gr-icons:check';
+        icon.color = 'green';
+        icon.tooltip = 'Passed';
+        break;
+    }
+    return icon;
+  }
 
-    _computeShowOnNeedsAndBlockedFilter(task) {
-      return this._isFailOrReadyOrInvalid(task) ||
-        (task.sub_tasks && task.sub_tasks.some(t =>
-          this._computeShowOnNeedsAndBlockedFilter(t)));
-    },
+  _isFailOrReadyOrInvalid(task) {
+    switch (task.status) {
+      case 'FAIL':
+      case 'READY':
+      case 'INVALID':
+        return true;
+    }
+    return false;
+  }
 
-    _compute_counts(task) {
-      this._all_count++;
-      switch (task.status) {
-        case 'FAIL':
-          this._fail_count++;
-          break;
-        case 'READY':
-          this._ready_count++;
-          break;
-      }
-    },
+  _computeShowOnNeedsAndBlockedFilter(task) {
+    return this._isFailOrReadyOrInvalid(task) ||
+      (task.sub_tasks && task.sub_tasks.some(t =>
+        this._computeShowOnNeedsAndBlockedFilter(t)));
+  }
 
-    _addTasks(tasks) { // rename to process, remove DOM bits
-      if (!tasks) return [];
-      tasks.forEach(task => {
-        task.message = task.hint || task.name;
-        task.icon = this._computeIcon(task);
-        task.showOnFilter = this._computeShowOnNeedsAndBlockedFilter(task);
-        this._compute_counts(task);
-        this._addTasks(task.sub_tasks);
-      });
-      return tasks;
-    },
+  _compute_counts(task) {
+    this._all_count++;
+    switch (task.status) {
+      case 'FAIL':
+        this._fail_count++;
+        break;
+      case 'READY':
+        this._ready_count++;
+        break;
+    }
+  }
 
-    _show_all_tap() {
-      this._show_all = 'true';
-      this._expand_all = true;
-    },
+  _addTasks(tasks) { // rename to process, remove DOM bits
+    if (!tasks) return [];
+    tasks.forEach(task => {
+      task.message = task.hint || task.name;
+      task.icon = this._computeIcon(task);
+      task.showOnFilter = this._computeShowOnNeedsAndBlockedFilter(task);
+      this._compute_counts(task);
+      this._addTasks(task.sub_tasks);
+    });
+    return tasks;
+  }
 
-    _needs_and_blocked_tap() {
-      this._show_all = 'false';
-      this._expand_all = true;
-    },
+  _show_all_tap() {
+    this._show_all = 'true';
+    this._expand_all = 'true';
+  }
 
-    _switch_expand() {
-      this._expand_all = !this._expand_all;
-    },
-  });
-})();
+  _needs_and_blocked_tap() {
+    this._show_all = 'false';
+    this._expand_all = 'true';
+  }
+
+  _switch_expand() {
+    this._expand_all = !this._expand_all;
+  }
+}
+
+customElements.define(GrTaskPlugin.is, GrTaskPlugin);
diff --git a/gr-task-plugin/gr-task-plugin_html.js b/gr-task-plugin/gr-task-plugin_html.js
new file mode 100644
index 0000000..71c995c
--- /dev/null
+++ b/gr-task-plugin/gr-task-plugin_html.js
@@ -0,0 +1,80 @@
+/**
+ * @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.
+ */
+
+export const htmlTemplate = Polymer.html`
+<style>
+  ul {
+    padding-left: 0.5em;
+    margin-top: 0;
+  }
+  h3 {
+    padding-left: 0.1em;
+    margin: 0 0 0 0;
+  }
+  .cursor { cursor: pointer; }
+  .links {
+    color: blue;
+    cursor: pointer;
+    text-decoration: underline;
+  }
+  #tasks_header {
+    align-items: center;
+    background-color: #fafafa;
+    border-top: 1px solid #ddd;
+    display: flex;
+    padding: 6px 1rem;
+  }
+  .no-margins { margin: 0 0 0 0; }
+</style>
+
+<div id="tasks" hidden$="[[!_tasks.length]]">
+  <div id="tasks_header" style="display: flex;">
+    <iron-icon
+        icon="gr-icons:expand-less"
+        hidden$="[[!_expand_all]]"
+        on-tap="_switch_expand"
+        class="cursor"> </iron-icon>
+    <iron-icon
+        icon="gr-icons:expand-more"
+        hidden$="[[_expand_all]]"
+        on-tap="_switch_expand"
+        class="cursor"> </iron-icon>
+    <div style="display: flex; align-items: center; column-gap: 1em;">
+    <h3 class="no-margins" on-tap="_switch_expand" class="cursor"> Tasks </h3>
+    <template is="dom-if" if="[[_is_show_all(_show_all)]]">
+      <p class="no-margins" >All ([[_all_count]]) |&nbsp;
+        <span
+            on-click="_needs_and_blocked_tap"
+            class="links">Needs + Blocked ([[_ready_count]], [[_fail_count]])</span>
+      <p>
+    </template>
+    <template is="dom-if" if="[[!_is_show_all(_show_all)]]">
+      <p class="no-margins" > <span
+            class="links"
+            on-click="_show_all_tap">All ([[_all_count]])</span>
+        &nbsp;| Needs + Blocked ([[_ready_count]], [[_fail_count]])</p>
+    </template>
+  </div>
+  </div>
+  <div hidden$="[[!_expand_all]]">
+    <ul style="list-style-type:none;">
+      <gr-task-plugin-tasks
+          tasks="[[_tasks]]"
+          show_all$="[[_show_all]]"> </gr-task-plugin-tasks>
+    </ul>
+  </div>
+</div>`;
diff --git a/gr-task-plugin/plugin.js b/gr-task-plugin/plugin.js
new file mode 100644
index 0000000..59d05b0
--- /dev/null
+++ b/gr-task-plugin/plugin.js
@@ -0,0 +1,23 @@
+/**
+ * @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.
+ */
+
+import './gr-task-plugin.js';
+
+Gerrit.install(plugin => {
+  plugin.registerCustomComponent(
+      'change-view-integration', 'gr-task-plugin');
+});
diff --git a/package.json b/package.json
index fe5067c..ff90a5b 100644
--- a/package.json
+++ b/package.json
@@ -1,14 +1,19 @@
 {
   "name": "task",
-  "version": "2.16.13-SNAPSHOT",
+  "version": "3.2.6-SNAPSHOT",
   "description": "Task Plugin",
-  "dependencies": {},
+  "dependencies": {
+    "@bazel/rollup": "^3.4.0",
+    "@bazel/terser": "^3.4.0"
+  },
   "devDependencies": {
     "eslint": "^6.6.0",
     "eslint-config-google": "^0.13.0",
     "eslint-plugin-html": "^6.0.0",
     "eslint-plugin-import": "^2.20.1",
-    "eslint-plugin-jsdoc": "^19.2.0"
+    "eslint-plugin-jsdoc": "^19.2.0",
+    "rollup": "^2.45.2",
+    "terser": "^5.6.1"
   },
   "scripts": {
     "eslint": "./node_modules/eslint/bin/eslint.js --ext .html,.js gr-task-plugin || exit 0",
diff --git a/plugin.html b/plugin.html
deleted file mode 100644
index 9d3144c..0000000
--- a/plugin.html
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-Copyright (C) 2019 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.
--->
-
-<link rel="import" href="./gr-task-plugin/gr-task-plugin.html">
-
-<dom-module id="task-plugin">
-  <script>
-    if (window.Polymer) {
-      Gerrit.install(function(plugin) {
-          plugin.registerCustomComponent(
-              'change-view-integration', 'gr-task-plugin');
-      });
-    }
-  </script>
-</dom-module>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
index 11786e6..3a8d903 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
@@ -19,7 +19,7 @@
 import com.google.gerrit.extensions.webui.JavaScriptPlugin;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.gerrit.server.DynamicOptions.DynamicBean;
-import com.google.gerrit.server.change.ChangeAttributeFactory;
+import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
 import com.google.gerrit.server.restapi.change.GetChange;
 import com.google.gerrit.server.restapi.change.QueryChanges;
 import com.google.gerrit.sshd.commands.Query;
@@ -34,7 +34,7 @@
   public static class Module extends AbstractModule {
     @Override
     protected void configure() {
-      bind(ChangeAttributeFactory.class)
+      bind(ChangePluginDefinedInfoFactory.class)
           .annotatedWith(Exports.named("task"))
           .to(TaskAttributeFactory.class);
 
@@ -42,7 +42,7 @@
       bind(DynamicBean.class).annotatedWith(Exports.named(Query.class)).to(MyOptions.class);
       bind(DynamicBean.class).annotatedWith(Exports.named(QueryChanges.class)).to(MyOptions.class);
       DynamicSet.bind(binder(), WebUiPlugin.class)
-          .toInstance(new JavaScriptPlugin("gr-task-plugin.html"));
+          .toInstance(new JavaScriptPlugin("gr-task-plugin.js"));
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
index 2ac26d8..1e19f7b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
@@ -15,11 +15,12 @@
 package com.googlesource.gerrit.plugins.task;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.PluginDefinedInfo;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.DynamicOptions.BeanProvider;
-import com.google.gerrit.server.change.ChangeAttributeFactory;
+import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
@@ -27,14 +28,15 @@
 import com.googlesource.gerrit.plugins.task.cli.PatchSetArgument;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
-public class TaskAttributeFactory implements ChangeAttributeFactory {
+public class TaskAttributeFactory implements ChangePluginDefinedInfoFactory {
   private static final FluentLogger log = FluentLogger.forEnclosingClass();
-
   public enum Status {
     INVALID,
     UNKNOWN,
@@ -76,15 +78,17 @@
   }
 
   @Override
-  public PluginDefinedInfo create(ChangeData c, BeanProvider beanProvider, String plugin) {
+  public Map<Change.Id, PluginDefinedInfo> createPluginDefinedInfos(
+          Collection<ChangeData> cds, BeanProvider beanProvider, String plugin) {
+    Map<Change.Id, PluginDefinedInfo> pluginInfosByChange = new HashMap<>();
     options = (Modules.MyOptions) beanProvider.getDynamicBean(plugin);
     if (options.all || options.onlyApplicable || options.onlyInvalid) {
       for (PatchSetArgument psa : options.patchSetArguments) {
         definitions.masquerade(psa);
       }
-      return createWithExceptions(c);
+      cds.forEach(cd -> pluginInfosByChange.put(cd.getId(), createWithExceptions(cd)));
     }
-    return null;
+    return pluginInfosByChange;
   }
 
   protected PluginDefinedInfo createWithExceptions(ChangeData c) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/cli/PatchSetArgument.java b/src/main/java/com/googlesource/gerrit/plugins/task/cli/PatchSetArgument.java
index e0e2317..6fb4e69 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/cli/PatchSetArgument.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/cli/PatchSetArgument.java
@@ -48,7 +48,7 @@
     public PatchSetArgument createForArgument(String token) {
       try {
         PatchSet.Id patchSetId = parsePatchSet(token);
-        ChangeNotes changeNotes = notesFactory.createChecked(patchSetId.changeId());
+        ChangeNotes changeNotes = notesFactory.createCheckedUsingIndexLookup(patchSetId.changeId());
         permissionBackend.user(user).change(changeNotes).check(ChangePermission.READ);
         return new PatchSetArgument(changeNotes.getChange(), psUtil.get(changeNotes, patchSetId));
       } catch (PermissionBackendException | AuthException e) {
diff --git a/test/docker/gerrit/Dockerfile b/test/docker/gerrit/Dockerfile
index 6d65974..b15542e 100755
--- a/test/docker/gerrit/Dockerfile
+++ b/test/docker/gerrit/Dockerfile
@@ -1,6 +1,4 @@
-FROM gerritcodereview/gerrit:3.2.10-ubuntu20
-
-USER root
+FROM gerritcodereview/gerrit:3.3.4-ubuntu20
 
 ENV GERRIT_SITE /var/gerrit
 RUN git config -f "$GERRIT_SITE/etc/gerrit.config" auth.type \
@@ -9,5 +7,3 @@
 COPY artifacts /tmp/
 RUN cp /tmp/task.jar "$GERRIT_SITE/plugins/task.jar"
 RUN { [ -e /tmp/gerrit.war ] && cp /tmp/gerrit.war "$GERRIT_SITE/bin/gerrit.war" ; } || true
-
-USER gerrit
diff --git a/tools/bzl/genrule2.bzl b/tools/bzl/genrule2.bzl
deleted file mode 100644
index e223dcd..0000000
--- a/tools/bzl/genrule2.bzl
+++ /dev/null
@@ -1,2 +0,0 @@
-load("@com_googlesource_gerrit_bazlets//tools:genrule2.bzl", _genrule2 = "genrule2")
-genrule2 = _genrule2
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index ec1e08a..e36a535 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -1,2 +1,2 @@
-load("@com_googlesource_gerrit_bazlets//tools:js.bzl", _polygerrit_plugin = "polygerrit_plugin")
-polygerrit_plugin = _polygerrit_plugin
+load("@com_googlesource_gerrit_bazlets//tools:js.bzl", _gerrit_js_bundle = "gerrit_js_bundle")
+gerrit_js_bundle = _gerrit_js_bundle
diff --git a/yarn.lock b/yarn.lock
index 0664ef6..0bdc3f3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -23,6 +23,16 @@
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
+"@bazel/rollup@^3.4.0":
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.5.0.tgz#3de2db08cbc62c3cffbbabaa4517ec250cf6419a"
+  integrity sha512-sFPqbzSbIn6h66uuZdXgK5oitSmEGtnDPfL3TwTS4ZWy75SpYvk9X1TFGlvkralEkVnFfdH15sq80/1t+YgQow==
+
+"@bazel/terser@^3.4.0":
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-3.5.0.tgz#4b1c3a3b781e65547694aa05bc600c251e4d8c0b"
+  integrity sha512-dpWHn1Iu+w0uA/kvPb0pP+4Io0PrVuzCCbVg2Ow4uRt/gTFKQJJWp4EiTitEZlPA2dHlW7PHThAb93lGo2c8qA==
+
 "@types/json5@^0.0.29":
   version "0.0.29"
   resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@@ -124,6 +134,11 @@
     balanced-match "^1.0.0"
     concat-map "0.0.1"
 
+buffer-from@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+  integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+
 call-bind@^1.0.0, call-bind@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@@ -195,6 +210,11 @@
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
+commander@^2.20.0:
+  version "2.20.3"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
 comment-parser@^0.7.2:
   version "0.7.6"
   resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-0.7.6.tgz#0e743a53c8e646c899a1323db31f6cd337b10f12"
@@ -323,21 +343,22 @@
     is-arrayish "^0.2.1"
 
 es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2:
-  version "1.18.5"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.5.tgz#9b10de7d4c206a3581fd5b2124233e04db49ae19"
-  integrity sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==
+  version "1.18.6"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.6.tgz#2c44e3ea7a6255039164d26559777a6d978cb456"
+  integrity sha512-kAeIT4cku5eNLNuUKhlmtuk1/TRZvQoYccn6TO0cSVdf1kzB0T7+dYuVK9MWM7l+/53W2Q8M7N2c6MQvhXFcUQ==
   dependencies:
     call-bind "^1.0.2"
     es-to-primitive "^1.2.1"
     function-bind "^1.1.1"
     get-intrinsic "^1.1.1"
+    get-symbol-description "^1.0.0"
     has "^1.0.3"
     has-symbols "^1.0.2"
     internal-slot "^1.0.3"
-    is-callable "^1.2.3"
+    is-callable "^1.2.4"
     is-negative-zero "^2.0.1"
-    is-regex "^1.1.3"
-    is-string "^1.0.6"
+    is-regex "^1.1.4"
+    is-string "^1.0.7"
     object-inspect "^1.11.0"
     object-keys "^1.1.1"
     object.assign "^4.1.2"
@@ -592,6 +613,11 @@
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
+fsevents@~2.3.1:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
 function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -611,6 +637,14 @@
     has "^1.0.3"
     has-symbols "^1.0.1"
 
+get-symbol-description@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6"
+  integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==
+  dependencies:
+    call-bind "^1.0.2"
+    get-intrinsic "^1.1.1"
+
 glob-parent@^5.0.0:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@@ -777,7 +811,7 @@
     call-bind "^1.0.2"
     has-tostringtag "^1.0.0"
 
-is-callable@^1.1.4, is-callable@^1.2.3:
+is-callable@^1.1.4, is-callable@^1.2.4:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
   integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
@@ -830,7 +864,7 @@
   dependencies:
     has-tostringtag "^1.0.0"
 
-is-regex@^1.1.3:
+is-regex@^1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
   integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
@@ -838,7 +872,7 @@
     call-bind "^1.0.2"
     has-tostringtag "^1.0.0"
 
-is-string@^1.0.5, is-string@^1.0.6:
+is-string@^1.0.5, is-string@^1.0.7:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
   integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
@@ -1207,6 +1241,13 @@
   dependencies:
     glob "^7.1.3"
 
+rollup@^2.45.2:
+  version "2.47.0"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.47.0.tgz#9d958aeb2c0f6a383cacc0401dff02b6e252664d"
+  integrity sha512-rqBjgq9hQfW0vRmz+0S062ORRNJXvwRpzxhFXORvar/maZqY6za3rgQ/p1Glg+j1hnc1GtYyQCPiAei95uTElg==
+  optionalDependencies:
+    fsevents "~2.3.1"
+
 run-async@^2.4.0:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
@@ -1269,6 +1310,24 @@
     astral-regex "^1.0.0"
     is-fullwidth-code-point "^2.0.0"
 
+source-map-support@~0.5.19:
+  version "0.5.19"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
+  integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map@^0.6.0:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+source-map@~0.7.2:
+  version "0.7.3"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
+  integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
+
 spdx-correct@^3.0.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
@@ -1382,6 +1441,15 @@
     slice-ansi "^2.1.0"
     string-width "^3.0.0"
 
+terser@^5.6.1:
+  version "5.7.0"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.0.tgz#a761eeec206bc87b605ab13029876ead938ae693"
+  integrity sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g==
+  dependencies:
+    commander "^2.20.0"
+    source-map "~0.7.2"
+    source-map-support "~0.5.19"
+
 text-table@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"