Use API exposed by gerritchangequery-plugin

The gerritchangequery-plugin for Jenkins makes job runs working on
a given patchset queryable. This makes searching for jobs relevant
to the checks-API much more efficient.

[1] https://review.gerrithub.io/admin/repos/tdraebing/gerritchangequery-plugin,general

Change-Id: I96906668be85e998f9a2aedcd5e87531f393d966
diff --git a/src/main/java/com/google/gerrit/plugins/checks/jenkins/GetConfig.java b/src/main/java/com/google/gerrit/plugins/checks/jenkins/GetConfig.java
index 245b5de..c6d71a7 100644
--- a/src/main/java/com/google/gerrit/plugins/checks/jenkins/GetConfig.java
+++ b/src/main/java/com/google/gerrit/plugins/checks/jenkins/GetConfig.java
@@ -30,7 +30,6 @@
 class GetConfig implements RestReadView<ProjectResource> {
   private static final String JENKINS_SECTION = "jenkins";
   private static final String JENKINS_URL_KEY = "url";
-  private static final String JENKINS_JOB_KEY = "job";
 
   private final PluginConfigFactory config;
   private final String pluginName;
@@ -50,7 +49,6 @@
       JenkinsChecksConfig jenkinsCfg = new JenkinsChecksConfig();
       jenkinsCfg.name = instance;
       jenkinsCfg.url = cfg.getString(JENKINS_SECTION, instance, JENKINS_URL_KEY);
-      jenkinsCfg.jobs = cfg.getStringList(JENKINS_SECTION, instance, JENKINS_JOB_KEY);
       result.add(jenkinsCfg);
     }
     return Response.ok(result);
@@ -59,9 +57,5 @@
   static class JenkinsChecksConfig {
     String name;
     String url;
-    //TODO(Thomas): It would be preferable to not have to configure any jobs, but
-    // to let Jenkins know which Jobs worked on a PatchSet. This will require
-    // additional changes in Jenkins however.
-    String[] jobs;
   }
 }
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 6d4f006..5633218 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -1,23 +1,17 @@
 Implementation of checks UI for Jenkins CI servers
+==================================================
 
 This plugin registers a `ChecksProvider` with the Gerrit UI that will fetch
 build results for a change from configured Jenkins servers and provide them to
 the checks panel in a change screen.
 
-Limitations
+Requirements
 -----------
 
-Currently, only multibranch-pipeline jobs using the Gerrit SCM-source provided
-by the link:https://plugins.jenkins.io/gerrit-code-review/[gerrit-code-review]-
-plugin are supported.
+The Jenkins servers have to support CORS-requests. This can for example be achieved
+by using a reverse proxy that sets the `Access-Control-Allow-Origin` and
+`Access-Control-Allow-Credentials` header.
 
-The Jenkins Remote Access API does not provide all the information that could
-be displayed in Gerrit, e.g. a result summary. Thus, as of now, this plugin
-does not make full use of the checks API. As of right now, it will display the
-following data in the UI:
-
-- Builds for the selected patchset including previous attempts
-- Status of the build
-- Result of the build
-- Link to the build and its logs
-- A result summary stating the result category used by the CI (e.g. `Result: UNSTABLE`)
+The Jenkins server additionally needs to install the
+[gerrit-change-query](https://review.gerrithub.io/admin/repos/tdraebing/gerritchangequery-plugin,general)
+plugin that enables querying for job runs working on a given patchset.
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index d87e2a0..50d9922 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -11,8 +11,3 @@
 
 jenkins.NAME.url
 : Base URL of Jenkins including protocol, e.g. https://gerrit-ci.gerritforge.com
-
-jenkins:NAME:job
-: Name of the multibranch pipeline job using the Gerrit SCM source that should
-  be queried for a build for a patchset, e.g. Gerrit-verifier-pipeline. Can be
-  defined multiple times.
diff --git a/web/fetcher.ts b/web/fetcher.ts
index 0eef435..a82eaca 100644
--- a/web/fetcher.ts
+++ b/web/fetcher.ts
@@ -15,12 +15,12 @@
  * limitations under the License.
  */
 import {
-  Category,
+  Action,
+  ActionResult,
   ChangeData,
   CheckResult,
   CheckRun,
   ChecksProvider,
-  LinkIcon,
   ResponseCode,
   RunStatus,
 } from '@gerritcodereview/typescript-api/checks';
@@ -29,23 +29,35 @@
 export declare interface Config {
   name: string;
   url: string;
-  jobs: string[];
 }
 
-export declare interface Job {
-  exists: boolean;
-  builds: Build[];
+export declare interface JenkinsCheckRun {
+  actions: JenkinsAction[];
+  attempt: number;
+  change: number;
+  checkDescription: string;
+  checkLink: string;
+  checkName: string;
+  externalId: string;
+  finishedTimestamp: string;
+  labelName: string;
+  patchset: number;
+  results: CheckResult[];
+  scheduledTimestamp: string;
+  startedTimestamp: string;
+  status: RunStatus;
+  statusDesciption: string;
+  statusLink: string;
 }
 
-export declare interface Build {
-  number: number;
-  url: string;
-}
-
-export declare interface BuildDetail {
-  number: number;
-  building: boolean;
-  result: string;
+export declare interface JenkinsAction {
+  data: string;
+  disabled: boolean;
+  method: string;
+  name: string;
+  primary: boolean;
+  summary: boolean;
+  tooltip: string;
   url: string;
 }
 
@@ -77,23 +89,17 @@
     }
     const checkRuns: CheckRun[] = [];
     for (const jenkins of this.configs) {
-      for (const jenkinsJob of jenkins.jobs) {
-        // TODO: Requests to Jenkins should be proxied through the Gerrit backend
-        // to avoid CORS requests.
-        const job: Job = await this.fetchJobInfo(
-          this.buildJobApiUrl(jenkins.url, jenkinsJob, changeData)
-        );
-
-        for (const build of job.builds) {
-          checkRuns.push(
-            this.convert(
-              jenkinsJob,
-              changeData,
-              await this.fetchBuildInfo(this.buildBuildApiUrl(build.url))
-            )
-          );
-        }
-      }
+      // TODO: Requests to Jenkins should be proxied through the Gerrit backend
+      // to avoid CORS requests.
+      await this.fetchFromJenkins(
+        `${jenkins.url}/gerrit/check-runs?change=${changeData.changeNumber}&patchset=${changeData.patchsetNumber}`
+      )
+        .then(response => response.json())
+        .then(data => {
+          data.runs.forEach((run: JenkinsCheckRun) => {
+            checkRuns.push(this.convert(run));
+          });
+        });
     }
 
     return {
@@ -102,33 +108,6 @@
     };
   }
 
-  buildJobApiUrl(
-    jenkinsUrl: string,
-    jenkinsJob: string,
-    changeData: ChangeData
-  ) {
-    let changeShard: string = changeData.changeNumber.toString().slice(-2);
-    if (changeShard.length === 1) {
-      changeShard = '0' + changeShard;
-    }
-    return (
-      jenkinsUrl +
-      '/job/' +
-      jenkinsJob +
-      '/job/' +
-      changeShard +
-      '%252F' +
-      changeData.changeNumber.toString() +
-      '%252F' +
-      changeData.patchsetNumber.toString() +
-      '/api/json?tree=builds[number,url]'
-    );
-  }
-
-  buildBuildApiUrl(baseUrl: string) {
-    return baseUrl + 'api/json?tree=number,result,building,url';
-  }
-
   fetchConfig(changeData: ChangeData): Promise<Config[]> {
     const pluginName = encodeURIComponent(this.plugin.getPluginName());
     return this.plugin
@@ -138,86 +117,54 @@
       );
   }
 
-  async fetchJobInfo(url: string): Promise<Job> {
-    let response: Response;
-    try {
-      response = await this.fetchFromJenkins(url);
-      if (!response.ok) {
-        throw response.statusText;
-      }
-    } catch (e) {
-      return {
-        exists: false,
-        builds: [],
-      };
-    }
-    const job: Job = await response.json();
-    return job;
-  }
-
-  async fetchBuildInfo(url: string): Promise<BuildDetail> {
-    const response = await this.fetchFromJenkins(url);
-    if (!response.ok) {
-      throw response.statusText;
-    }
-    const build: BuildDetail = await response.json();
-    return build;
-  }
-
-  convert(
-    checkName: string,
-    changeData: ChangeData,
-    build: BuildDetail
-  ): CheckRun {
-    let status: RunStatus;
-    const results: CheckResult[] = [];
-
-    if (build.result !== null) {
-      status = RunStatus.COMPLETED;
-      let resultCategory: Category;
-      switch (build.result) {
-        case 'SUCCESS':
-          resultCategory = Category.SUCCESS;
-          break;
-        case 'FAILURE':
-          resultCategory = Category.ERROR;
-          break;
-        default:
-          resultCategory = Category.WARNING;
-      }
-
-      const checkResult: CheckResult = {
-        category: resultCategory,
-        summary: `Result: ${build.result}`,
-        links: [
-          {
-            url: build.url + '/console',
-            primary: true,
-            icon: LinkIcon.EXTERNAL,
-          },
-        ],
-      };
-      results.push(checkResult);
-    } else if (build.building) {
-      status = RunStatus.RUNNING;
-    } else {
-      status = RunStatus.RUNNABLE;
-    }
-
-    const run: CheckRun = {
-      change: changeData.changeNumber,
-      patchset: changeData.patchsetNumber,
-      attempt: build.number,
-      checkName,
-      checkLink: build.url,
-      status,
-      results,
+  convert(run: JenkinsCheckRun): CheckRun {
+    const convertedRun: CheckRun = {
+      attempt: run.attempt,
+      change: run.change,
+      checkDescription: run.checkDescription,
+      checkLink: run.checkLink,
+      checkName: run.checkName,
+      externalId: run.externalId,
+      finishedTimestamp: new Date(run.finishedTimestamp),
+      labelName: run.labelName,
+      patchset: run.patchset,
+      results: run.results,
+      scheduledTimestamp: new Date(run.scheduledTimestamp),
+      startedTimestamp: new Date(run.startedTimestamp),
+      status: run.status,
+      statusDescription: run.statusDesciption,
+      statusLink: run.statusLink,
     };
-    return run;
+    const actions: Action[] = [];
+    for (const action of run.actions) {
+      actions.push({
+        name: action.name,
+        tooltip: action.tooltip,
+        primary: action.primary,
+        summary: action.summary,
+        disabled: action.disabled,
+        callback: () => this.rerun(action.url),
+      });
+    }
+    convertedRun.actions = actions;
+    return convertedRun;
   }
 
   private fetchFromJenkins(url: string): Promise<Response> {
-    const options: RequestInit = { credentials: 'include' };
+    const options: RequestInit = {credentials: 'include'};
     return fetch(url, options);
   }
+
+  private rerun(url: string): Promise<ActionResult> {
+    return this.fetchFromJenkins(url)
+      .then(_ => {
+        return {
+          message: 'Run triggered.',
+          shouldReload: true,
+        };
+      })
+      .catch(e => {
+        return {message: `Triggering the run failed: ${e.message}`};
+      });
+  }
 }