Merge branch 'stable-3.2' into stable-3.3

* stable-3.2:
  fixup!: docker: Enable reruning tests without bringing down setup
  docker: Enable reruning tests without bringing down setup
  docker: Add support for "--preserve" option

Change-Id: Idd1fa46aa6e75ab65a7edc5f90442cc6f8ddc6b6
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"