Fetch and render plugin license from Gerrit-CI

Enrich the plugin information with a new 'license' field that captures
the first 2 lines of the LICENSE file published by Gerrit-CI.

All Gerrit core plugins are associated with the Apache 2.0 license.

Whenever a plugin exposes a LICENSE file in its source code, the plugin
manager displays a "legal" icon that shows the license type on mouse
hover.

Also add test coverage for JenkinsCiPluginsRepository for fetching the
plugin information from a successful Jenkins Job build run.

Old plugin card : https://www.pasteboard.co/s87jpkcCcKxa.png
New plugin card : https://www.pasteboard.co/ZG3qMXgQqY81.png
Hover on license: https://www.pasteboard.co/cYAPqhlB2HZY.png

Change-Id: Ieb274a7f236c0c6d3a03db07838b5ea2a1926767
(cherry picked from commit 37f52d04052f6e763e45e74bd4c21f2947b8f981)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartJson.java b/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartJson.java
index 9dd1b6d..2da0fb0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartJson.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartJson.java
@@ -14,7 +14,9 @@
 
 package com.googlesource.gerrit.plugins.manager.gson;
 
+import com.google.gson.Gson;
 import com.google.gson.JsonElement;
+import java.util.Map;
 import java.util.Optional;
 import java.util.function.Function;
 
@@ -30,6 +32,10 @@
     return new SmartJson(fromJson);
   }
 
+  public static SmartJson of(Map<?, ?> jsonTreeMap) {
+    return of(new Gson().toJsonTree(jsonTreeMap, Map.class));
+  }
+
   public Optional<String> getOptionalString(String fieldName) {
     return getOptional(fieldName)
         .map(
@@ -66,4 +72,8 @@
   public JsonElement get() {
     return jsonElem;
   }
+
+  public void add(String property, JsonElement value) {
+    jsonElem.getAsJsonObject().add(property, value);
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsRepository.java b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsRepository.java
index 2e175ea..767d3ca 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsRepository.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsRepository.java
@@ -37,6 +37,12 @@
 import java.util.jar.Manifest;
 
 public class CorePluginsRepository implements PluginsRepository {
+  public static final String GERRIT_LICENSE =
+      """
+            Apache License
+      Version 2.0, January 2004\
+      """;
+
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   private static final String GERRIT_VERSION = Version.getVersion();
 
@@ -75,7 +81,8 @@
                       pluginsDescriptions.get(pluginName).orElse(""),
                       pluginAttributes.getValue("Implementation-Version"),
                       "",
-                      pluginUrl.toString());
+                      pluginUrl.toString(),
+                      GERRIT_LICENSE);
                 })
             .orElseGet(
                 () ->
@@ -84,7 +91,8 @@
                         "",
                         "",
                         "",
-                        pluginUrl.toString()));
+                        pluginUrl.toString(),
+                        GERRIT_LICENSE));
       } catch (IOException e) {
         logger.atSevere().withCause(e).log("Unable to open plugin %s", pluginUrl);
         return null;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java
index dc3ffd2..44c690d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java
@@ -40,6 +40,9 @@
 public class JenkinsCiPluginsRepository implements PluginsRepository {
 
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final int FETCH_ALL_LINES = 0;
+  private static final int FETCH_LICENSE_LINES = 2;
+  public static final String LICENSE_FILE = "LICENSE";
 
   private final PluginManagerConfig config;
 
@@ -159,6 +162,8 @@
 
     Optional<String> pluginVersion =
         fetchArtifact(buildExecution.get(), artifacts.get(), ".jar-version");
+    Optional<String> pluginLicense =
+        fetchArtifact(buildExecution.get(), artifacts.get(), LICENSE_FILE, FETCH_LICENSE_LINES);
     Optional<String> pluginDescription =
         fetchArtifactJson(buildExecution.get(), artifacts.get(), ".json")
             .flatMap(json -> json.getOptionalString("description"));
@@ -171,7 +176,13 @@
         String sha1 = lastBuildRevision.get().getString("SHA1").substring(0, 8);
         return pluginVersion.map(
             version ->
-                new PluginInfo(pluginName, pluginDescription.orElse(""), version, sha1, pluginUrl));
+                new PluginInfo(
+                    pluginName,
+                    pluginDescription.orElse(""),
+                    version,
+                    sha1,
+                    pluginUrl,
+                    pluginLicense.orElse(null)));
       }
     }
 
@@ -192,6 +203,8 @@
 
     Optional<String> pluginVersion =
         fetchArtifact(buildExecution.get(), artifacts.get(), ".js-version");
+    Optional<String> pluginLicense =
+        fetchArtifact(buildExecution.get(), artifacts.get(), LICENSE_FILE, FETCH_LICENSE_LINES);
     Optional<String> pluginDescription =
         fetchArtifactJson(buildExecution.get(), artifacts.get(), ".json")
             .flatMap(json -> json.getOptionalString("description"));
@@ -204,7 +217,13 @@
         String sha1 = lastBuildRevision.get().getString("SHA1").substring(0, 8);
         return pluginVersion.map(
             version ->
-                new PluginInfo(pluginName, pluginDescription.orElse(""), version, sha1, pluginUrl));
+                new PluginInfo(
+                    pluginName,
+                    pluginDescription.orElse(""),
+                    version,
+                    sha1,
+                    pluginUrl,
+                    pluginLicense.orElse(null)));
       }
     }
 
@@ -213,6 +232,11 @@
 
   private Optional<String> fetchArtifact(
       SmartJson buildExecution, JsonArray artifacts, String artifactSuffix) {
+    return fetchArtifact(buildExecution, artifacts, artifactSuffix, FETCH_ALL_LINES);
+  }
+
+  private Optional<String> fetchArtifact(
+      SmartJson buildExecution, JsonArray artifacts, String artifactSuffix, int maxLines) {
     StringBuilder artifactBody = new StringBuilder();
     Optional<SmartJson> verArtifactJson = findArtifact(artifacts, artifactSuffix);
     if (verArtifactJson.isPresent()) {
@@ -223,7 +247,9 @@
       try (BufferedReader reader =
           new BufferedReader(gsonProvider.get().getReader(versionUrl), 4096)) {
         String line;
-        while ((line = reader.readLine()) != null) {
+        for (int numLines = 0;
+            (maxLines == 0 || numLines < maxLines) && (line = reader.readLine()) != null;
+            numLines++) {
           if (artifactBody.length() > 0) {
             artifactBody.append("\n");
           }
@@ -252,7 +278,7 @@
   @VisibleForTesting
   Optional<SmartJson> tryGetJson(String url) {
     try {
-      return Optional.of(gsonProvider.get().get(url));
+      return Optional.ofNullable(gsonProvider.get().get(url));
     } catch (JsonSyntaxException jse) {
       logger.atWarning().withCause(jse).log("Invalid JSON from %s", url);
     } catch (IOException e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/PluginInfo.java b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/PluginInfo.java
index ac30a64..a2683f5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/PluginInfo.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/PluginInfo.java
@@ -14,6 +14,7 @@
 
 package com.googlesource.gerrit.plugins.manager.repository;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.Url;
 
 public class PluginInfo {
@@ -23,13 +24,21 @@
   public final String version;
   public final String sha1;
   public final String url;
+  public final String license;
 
-  public PluginInfo(String name, String description, String version, String sha1, String url) {
+  public PluginInfo(
+      String name,
+      String description,
+      String version,
+      String sha1,
+      String url,
+      @Nullable String license) {
     this.id = Url.encode(name);
     this.name = name;
     this.description = description;
     this.version = version;
     this.sha1 = sha1;
     this.url = url;
+    this.license = license;
   }
 }
diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css
index fbf00be..35cb664 100644
--- a/src/main/resources/static/css/style.css
+++ b/src/main/resources/static/css/style.css
@@ -64,6 +64,57 @@
   border-radius: 0.5rem;
   flex-shrink: 0;
 }
+.license-icon-wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 36px;
+  height: 36px;
+  background-color: var(--bg-light);
+  color: var(--primary-blue);
+  border-radius: 0.5rem;
+  flex-shrink: 0;
+}
+/* Push the footer to the bottom and space children horizontally */
+.feature-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: auto;
+  width: 100%;
+}
+
+/* Updated License Container */
+.feature-license {
+  position: relative;
+  display: inline-flex;
+  cursor: help;
+}
+
+/* Tooltip base state (Hidden) */
+.feature-license .license-popup {
+  visibility: hidden;
+  opacity: 0;
+  position: absolute;
+  bottom: 120%;
+  right: 0;
+  background-color: var(--primary-blue);
+  color: white;
+  padding: 0.4rem 0.75rem;
+  border-radius: 0.5rem;
+  font-size: 0.75rem;
+  white-space: nowrap;
+  pointer-events: none;
+  transition: opacity 0.2s ease, visibility 0.2s ease;
+  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+  z-index: 10;
+}
+
+/* Tooltip hover state (Visible) */
+.feature-license:hover .license-popup {
+  visibility: visible;
+  opacity: 1;
+}
 .feature-title {
   margin-bottom: 0;
 }
diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html
index 7f3468c..0a8cb3b 100644
--- a/src/main/resources/static/index.html
+++ b/src/main/resources/static/index.html
@@ -79,28 +79,37 @@
 
               <p class="feature-text">{{prop.description.split('.')[0]}}</p>
 
-              <div class="feature-actions">
-                  <!-- Status Labels -->
-                  <span id="installing-{{prop.id}}" class="status-label status-default hidden">Installing...</span>
-                  <span id="installed-{{prop.id}}"
-                        class="status-label status-success {{ (prop.version != '' && prop.update_version == '') ? '':'hidden' }}">Up to date</span>
-                  <span id="failed-{{prop.id}}" class="status-label status-warning hidden">Failed</span>
+              <div class="feature-footer">
+                    <div class="feature-actions">
+                        <!-- Status Labels -->
+                        <span id="installing-{{prop.id}}" class="status-label status-default hidden">Installing...</span>
+                        <span id="installed-{{prop.id}}"
+                              class="status-label status-success {{ (prop.version != '' && prop.update_version == '') ? '':'hidden' }}">Up to date</span>
+                        <span id="failed-{{prop.id}}" class="status-label status-warning hidden">Failed</span>
 
-                  <!-- Action Buttons -->
-                  <button id="install-{{prop.id}}" type="button"
-                          class="install-btn card-btn {{ (prop.version == '' && prop.update_version != undefined) ? '':'hidden' }}"
-                          ng-click="install(prop.id,prop.url)">
-                      Install
-                  </button>
+                        <!-- Action Buttons -->
+                        <button id="install-{{prop.id}}" type="button"
+                                class="install-btn card-btn {{ (prop.version == '' && prop.update_version != undefined) ? '':'hidden' }}"
+                                ng-click="install(prop.id,prop.url)">
+                            Install
+                        </button>
 
-                  <button id="upgrade-{{prop.id}}" type="button"
-                          class="install-btn card-btn {{ (prop.version != '' && prop.update_version != '') ? '':'hidden' }}"
-                          style="{{ (prop.version != '' && prop.update_version != '') ? 'background-color: #f97316; box-shadow: 0 4px 6px -1px rgba(249, 115, 22, 0.4);' : '' }}"
-                          onmouseover="this.style.backgroundColor='#ea580c'"
-                          onmouseout="this.style.backgroundColor='#f97316'"
-                          ng-click="install(prop.id,prop.url)">
-                      Upgrade
-                  </button>
+                        <button id="upgrade-{{prop.id}}" type="button"
+                                class="install-btn card-btn {{ (prop.version != '' && prop.update_version != '') ? '':'hidden' }}"
+                                style="{{ (prop.version != '' && prop.update_version != '') ? 'background-color: #f97316; box-shadow: 0 4px 6px -1px rgba(249, 115, 22, 0.4);' : '' }}"
+                                onmouseover="this.style.backgroundColor='#ea580c'"
+                                onmouseout="this.style.backgroundColor='#f97316'"
+                                ng-click="install(prop.id,prop.url)">
+                            Upgrade
+                        </button>
+                    </div>
+
+                    <div class="feature-license" ng-if="prop.license && prop.license.length > 0">
+                        <div class="license-icon-wrapper" tabindex="0" aria-label="{{ prop.license }}">
+                            <svg data-component="Octicon" aria-hidden="true" focusable="false" class="octicon octicon-law mr-2" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" display="inline-block" overflow="visible" style="vertical-align:text-bottom"><path d="M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C14.556 10.78 13.88 11 13 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L12.178 4.5h-.162c-.305 0-.604-.079-.868-.231l-1.29-.736a.245.245 0 0 0-.124-.033H8.75V13h2.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.5V3.5h-.984a.245.245 0 0 0-.124.033l-1.289.737c-.265.15-.564.23-.869.23h-.162l2.112 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C4.556 10.78 3.88 11 3 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L2.178 4.5H1.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.288-.737c.265-.15.564-.23.869-.23h.984V.75a.75.75 0 0 1 1.5 0Zm2.945 8.477c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327Zm-10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327Z"></path></svg>
+                        </div>
+                        <span class="license-popup">{{ prop.license }}</span>
+                    </div>
               </div>
           </div>
       </div>
diff --git a/src/main/resources/static/js/plugin-manager.js b/src/main/resources/static/js/plugin-manager.js
index 3b1e5a4..20a8d6c 100644
--- a/src/main/resources/static/js/plugin-manager.js
+++ b/src/main/resources/static/js/plugin-manager.js
@@ -63,18 +63,20 @@
                       version : plugin.version,
                       sha1 : '',
                       url : plugin.url,
-                      update_version : ''
+                      update_version : '',
+                      license: plugin.license,
                     });
                   } else {
                     plugins.list[currPluginIdx] = {
-                      id : plugin.id,
-                      description : plugin.description,
-                      index_url : plugin.index_url,
-                      version : plugin.version,
-                      sha1 : '',
-                      url : plugin.url,
-                      update_version : ''
-                    }
+                      id: plugin.id,
+                      description: plugin.description,
+                      index_url: plugin.index_url,
+                      version: plugin.version,
+                      sha1: "",
+                      url: plugin.url,
+                      update_version: "",
+                      license: plugin.license,
+                    };
                   }
                 }
               });
@@ -110,6 +112,7 @@
                       currPlugin.sha1 = plugin.sha1;
                       currPlugin.url = plugin.url;
                       currPlugin.description = plugin.description;
+                      currPlugin.license = plugin.license;
 
                       if (currRow < 0) {
                         plugins.list.push(currPlugin);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepositoryTest.java b/src/test/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepositoryTest.java
new file mode 100644
index 0000000..c30f1ba
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepositoryTest.java
@@ -0,0 +1,224 @@
+// Copyright (C) 2026 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.
+
+package com.googlesource.gerrit.plugins.manager.repository;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.plugins.manager.repository.JenkinsCiPluginsRepository.LICENSE_FILE;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
+import com.google.gson.JsonObject;
+import com.google.inject.util.Providers;
+import com.googlesource.gerrit.plugins.manager.PluginManagerConfig;
+import com.googlesource.gerrit.plugins.manager.gson.SmartGson;
+import com.googlesource.gerrit.plugins.manager.gson.SmartJson;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.ArrayUtils;
+import org.jspecify.annotations.NonNull;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class JenkinsCiPluginsRepositoryTest {
+  private static final String TEST_GERRIT_VERSION = "3.0";
+  private static final String TEST_JENKINS_URL = "http://jenkins.test.localdomain";
+  private static final String JENKINS_JSON_API = "/api/json";
+  private static final String JENKINS_JOB_SUCCESS_COLOR = "blue";
+
+  private static final String TEST_JAR_PLUGIN_NAME = "test-jar-plugin";
+  private static final String TEST_JS_PLUGIN_NAME = "test-js-plugin";
+  private static final String TEST_JENKINS_JOB_BUILD_SHA1 =
+      "59e8a0d32c1252f7df949d9377ceab0b39d6339f";
+  private static final String FOO_LICENSE =
+      """
+      Foo License 2.0
+      April 2005
+      This is foo
+      """;
+  private static final String BAR_LICENSE =
+      """
+      Bar License 2.0
+      April 2006
+      This is foo
+      """;
+  private static final String FOO_LICENSE_FIRST_TWO_LINES =
+      FOO_LICENSE.lines().limit(2).collect(Collectors.joining("\n"));
+  private static final String BAR_LICENSE_FIRST_TWO_LINES =
+      BAR_LICENSE.lines().limit(2).collect(Collectors.joining("\n"));
+  private static final String TEST_PLUGIN_VERSION_1_0 = "v1.0";
+  private static final String TEST_PLUGIN_DESCRIPTION = "Test plugin";
+
+  @Mock SmartGson gsonMock;
+
+  @Mock PluginManagerConfig configMock;
+
+  private JenkinsCiPluginsRepository.View testView;
+
+  @Before
+  public void setUp() throws IOException {
+    testView = new JenkinsCiPluginsRepository.View();
+    testView.jobs = new JenkinsCiPluginsRepository.Job[0];
+    when(gsonMock.get(any(String.class), eq(JenkinsCiPluginsRepository.View.class)))
+        .thenReturn(testView);
+    when(configMock.getJenkinsUrl()).thenReturn(TEST_JENKINS_URL);
+  }
+
+  @Test
+  public void noArtifactsPublished_listOfPluginShouldBeEmpty() throws IOException {
+    JenkinsCiPluginsRepository pluginRepo =
+        new JenkinsCiPluginsRepository(Providers.of(gsonMock), configMock);
+    assertThat(pluginRepo.list(TEST_GERRIT_VERSION)).isEmpty();
+  }
+
+  @Test
+  public void artifactPublishedWithLicense_listOfPluginsShouldHaveOnePluginWithLicense()
+      throws IOException {
+    addPluginToJenkinsCiMock(
+        TEST_JAR_PLUGIN_NAME, "jar", TEST_PLUGIN_VERSION_1_0, TEST_PLUGIN_DESCRIPTION, FOO_LICENSE);
+    addPluginToJenkinsCiMock(
+        TEST_JS_PLUGIN_NAME, "js", TEST_PLUGIN_VERSION_1_0, TEST_PLUGIN_DESCRIPTION, BAR_LICENSE);
+
+    JenkinsCiPluginsRepository pluginRepo =
+        new JenkinsCiPluginsRepository(Providers.of(gsonMock), configMock);
+
+    List<PluginInfo> pluginsList = pluginRepo.list(TEST_GERRIT_VERSION);
+    assertThat(pluginsList).hasSize(2);
+
+    PluginInfo jarPluginInfo = getPluginWithName(pluginsList, TEST_JAR_PLUGIN_NAME);
+    assertThat(jarPluginInfo).isNotNull();
+    assertPluginInfoDetails(
+        jarPluginInfo,
+        TEST_PLUGIN_VERSION_1_0,
+        TEST_PLUGIN_DESCRIPTION,
+        FOO_LICENSE_FIRST_TWO_LINES);
+
+    PluginInfo jsPluginInfo = getPluginWithName(pluginsList, TEST_JS_PLUGIN_NAME);
+    assertThat(jsPluginInfo).isNotNull();
+    assertPluginInfoDetails(
+        jsPluginInfo,
+        TEST_PLUGIN_VERSION_1_0,
+        TEST_PLUGIN_DESCRIPTION,
+        BAR_LICENSE_FIRST_TWO_LINES);
+  }
+
+  @Test
+  public void artifactPublishedWithoutLicense_pluginLicenseShouldBeNull() throws IOException {
+    addPluginToJenkinsCiMock(
+        TEST_JAR_PLUGIN_NAME, "jar", TEST_PLUGIN_VERSION_1_0, TEST_PLUGIN_DESCRIPTION, null);
+
+    PluginInfo plugin =
+        new JenkinsCiPluginsRepository(Providers.of(gsonMock), configMock)
+            .list(TEST_GERRIT_VERSION)
+            .getFirst();
+
+    assertThat(plugin.license).isNull();
+  }
+
+  private static @Nullable PluginInfo getPluginWithName(
+      List<PluginInfo> pluginsList, String pluginName) {
+    return Iterables.tryFind(pluginsList, pi -> pi.name.equals(pluginName)).orNull();
+  }
+
+  private static void assertPluginInfoDetails(
+      PluginInfo pluginInfo, String pluginVersion, String pluginDescription, String pluginLicense) {
+    assertThat(pluginInfo.version).isEqualTo(pluginVersion);
+    assertThat(pluginInfo.description).isEqualTo(pluginDescription);
+    assertThat(pluginInfo.license).isEqualTo(pluginLicense);
+  }
+
+  private void addPluginToJenkinsCiMock(
+      String pluginName,
+      String pluginFileExtension,
+      String pluginVersion,
+      String pluginDescription,
+      @Nullable String pluginLicense)
+      throws IOException {
+    JenkinsCiPluginsRepository.Job testPluginJob = new JenkinsCiPluginsRepository.Job();
+    testPluginJob.color = JENKINS_JOB_SUCCESS_COLOR;
+    testPluginJob.name = pluginName + "-job";
+    testPluginJob.url = TEST_JENKINS_URL + "/job/" + testPluginJob.name;
+
+    testView.jobs = ArrayUtils.addAll(testView.jobs, testPluginJob);
+
+    String testPluginJobLastSuccessfulBuild = testPluginJob.url + "/lastSuccessfulBuild";
+
+    when(gsonMock.get(eq(testPluginJob.url + JENKINS_JSON_API)))
+        .thenReturn(
+            SmartJson.of(
+                Map.of("lastSuccessfulBuild", Map.of("url", testPluginJobLastSuccessfulBuild))));
+
+    String pluginVersionResourcePath = pluginName + "." + pluginFileExtension + "-version";
+    String pluginJsonResourcePath = pluginName + ".json";
+    SmartJson testBuildDetailJson =
+        SmartJson.of(
+            Map.of(
+                "artifacts",
+                List.of(
+                    jsonArtifact(pluginName, pluginName + "." + pluginFileExtension),
+                    jsonArtifact(pluginName, pluginVersionResourcePath),
+                    jsonArtifact(pluginName, pluginJsonResourcePath),
+                    jsonArtifact(pluginName, LICENSE_FILE)),
+                "url",
+                testPluginJobLastSuccessfulBuild,
+                "actions",
+                List.of(Map.of("lastBuiltRevision", Map.of("SHA1", TEST_JENKINS_JOB_BUILD_SHA1)))));
+    when(gsonMock.get(eq(testPluginJobLastSuccessfulBuild + JENKINS_JSON_API)))
+        .thenReturn(testBuildDetailJson);
+
+    when(gsonMock.getReader(
+            getArtifactUrl(
+                testPluginJobLastSuccessfulBuild, pluginName, pluginVersionResourcePath)))
+        .thenReturn(inputStreamReaderOf(pluginVersion));
+    if (pluginLicense != null) {
+      when(gsonMock.getReader(
+              getArtifactUrl(testPluginJobLastSuccessfulBuild, pluginName, LICENSE_FILE)))
+          .thenReturn(inputStreamReaderOf(pluginLicense));
+    }
+    when(gsonMock.get(
+            getArtifactUrl(testPluginJobLastSuccessfulBuild, pluginName, pluginJsonResourcePath)))
+        .thenReturn(SmartJson.of(Map.of("description", pluginDescription)));
+  }
+
+  private static @NonNull String getArtifactUrl(
+      String lastSuccessfulBuildUrl, String pluginName, String artifactPath) {
+    return lastSuccessfulBuildUrl + "/artifact/" + pluginName + "/" + artifactPath;
+  }
+
+  private InputStreamReader inputStreamReaderOf(String streamContent) {
+    return new InputStreamReader(
+        new ByteArrayInputStream(streamContent.getBytes(StandardCharsets.UTF_8)),
+        StandardCharsets.UTF_8);
+  }
+
+  private JsonObject jsonArtifact(String pluginName, String artifactName) {
+    JsonObject artifactJson = new JsonObject();
+    artifactJson.addProperty("displayPath", artifactName);
+    artifactJson.addProperty("fileName", artifactName);
+    artifactJson.addProperty("relativePath", pluginName + "/" + artifactName);
+    return artifactJson;
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/manager/repository/PluginsRepositoryTest.java b/src/test/java/com/googlesource/gerrit/plugins/manager/repository/PluginsRepositoryTest.java
index 0e14aa1..1ac70a3 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/manager/repository/PluginsRepositoryTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/manager/repository/PluginsRepositoryTest.java
@@ -29,6 +29,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Collection;
+import java.util.stream.Collectors;
 import org.junit.Test;
 
 public class PluginsRepositoryTest {
@@ -58,6 +59,8 @@
     assertThat(plugins.stream().map(p -> p.name).sorted().collect(toList()))
         .containsExactlyElementsIn(GERRIT_CORE_PLUGINS)
         .inOrder();
+    assertThat(plugins.stream().map(p -> p.license).collect(Collectors.toSet()))
+        .containsExactly(CorePluginsRepository.GERRIT_LICENSE);
   }
 
   @Test