Merge branch 'stable-3.8' into stable-3.9

* stable-3.8:
  GitHubOAuthConfigTest: Reformat the code using google-java-format
  Build github plugin against the v3.8.0-rc5 Gerrit API
  PullRequestCreateChange: Remove unused import
  Set plugin version to 3.4.7
  Fire git ref update events for all imported refs
  PluginVelocityRuntimeProvider: Fix warning flagged by error prone
  Fix default scopes resolution

Change-Id: Ifd589f9099f27ad59790e44f4cbca3f39327a767
diff --git a/README.md b/README.md
index 5a95019..c497a89 100644
--- a/README.md
+++ b/README.md
@@ -78,13 +78,13 @@
 Gerrit 3.3 is distributed for Java 11 only. However, the source code is compatible
 with Java 8 assuming you build it from the source repository by yourself.
 
-The GitHub plugin can be built for Java 8 by using the `javaVersion=1.8` Maven
+The GitHub plugin can be built for Java 17 by using the `javaVersion=1.17` Maven
 parameter.
 
 Example:
   git clone https://gerrit.googlesource.com/plugins/github
   cd github
-  mvn -DjavaVersion=1.8 install
+  mvn -DjavaVersion=17 install
 
 ### singleusergroup plugin
 
@@ -136,9 +136,10 @@
 * GitHub Integration
 
 * GitHub URL: [https://github.com]: <confirm>
-* Use GitHub for Gerrit login? [Y/n] Y
+* GitHub API URL: [https://api.github.com]: <confirm>
 * ClientId []: <provided client id from previous step>
 * ClientSecret []: <provided client secret from previous step>
+* Gerrit OAuth implementation [http/?]: <http or oauth>
 
 ### Receiving Pull Request events to automatically import
 
@@ -195,4 +196,4 @@
   refs/for/foo, refs/meta/bar
 ```
 
-More information on Gerrit magic refs can be found [here](https://gerrit-review.googlesource.com/Documentation/intro-user.html#upload-change)
\ No newline at end of file
+More information on Gerrit magic refs can be found [here](https://gerrit-review.googlesource.com/Documentation/intro-user.html#upload-change)
diff --git a/github-oauth/pom.xml b/github-oauth/pom.xml
index 75b9c09..d027b61 100644
--- a/github-oauth/pom.xml
+++ b/github-oauth/pom.xml
@@ -21,7 +21,7 @@
   <parent>
     <groupId>com.googlesource.gerrit.plugins.github</groupId>
     <artifactId>github-parent</artifactId>
-    <version>3.8.0</version>
+    <version>3.9.0-rc5</version>
   </parent>
   <artifactId>github-oauth</artifactId>
   <name>Gerrit Code Review - GitHub OAuth login</name>
@@ -71,35 +71,35 @@
     <dependency>
       <groupId>javax.servlet</groupId>
       <artifactId>javax.servlet-api</artifactId>
-      <version>3.0.1</version>
+      <version>3.1.0</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>com.google.inject</groupId>
       <artifactId>guice</artifactId>
-      <version>4.1.0</version>
+      <version>6.0.0</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
-      <version>20.0</version>
+      <version>32.1.2-jre</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.kohsuke</groupId>
       <artifactId>github-api</artifactId>
-      <version>1.116</version>
+      <version>1.316</version>
     </dependency>
     <dependency>
       <groupId>com.infradna.tool</groupId>
       <artifactId>bridge-method-injector</artifactId>
-      <version>1.18</version>
+      <version>1.29</version>
     </dependency>
     <dependency>
       <groupId>org.apache.httpcomponents</groupId>
       <artifactId>httpclient</artifactId>
-      <version>4.4</version>
+      <version>4.5.2</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/CanonicalWebUrls.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/CanonicalWebUrls.java
new file mode 100644
index 0000000..faca0f9
--- /dev/null
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/CanonicalWebUrls.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2023 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.github.oauth;
+
+import static com.googlesource.gerrit.plugins.github.oauth.GitHubOAuthConfig.GERRIT_OAUTH_FINAL;
+import static com.googlesource.gerrit.plugins.github.oauth.GitHubOAuthConfig.GITHUB_PLUGIN_OAUTH_SCOPE;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.MoreObjects;
+import com.google.gerrit.httpd.HttpCanonicalWebUrlProvider;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class CanonicalWebUrls {
+  private final GitHubOAuthConfig oauthConf;
+  private final HttpCanonicalWebUrlProvider canonicalWebUrlProvider;
+
+  static String trimTrailingSlash(String url) {
+    return CharMatcher.is('/').trimTrailingFrom(url);
+  }
+
+  @Inject
+  CanonicalWebUrls(
+      GitHubOAuthConfig oauthConf, HttpCanonicalWebUrlProvider canonicalWebUrlProvider) {
+    this.oauthConf = oauthConf;
+    this.canonicalWebUrlProvider = canonicalWebUrlProvider;
+  }
+
+  public String getScopeSelectionUrl() {
+    return getCannonicalWebUrl()
+        + MoreObjects.firstNonNull(oauthConf.scopeSelectionUrl, GITHUB_PLUGIN_OAUTH_SCOPE);
+  }
+
+  String getOAuthFinalRedirectUrl() {
+    return getCannonicalWebUrl() + GERRIT_OAUTH_FINAL;
+  }
+
+  private String getCannonicalWebUrl() {
+    return trimTrailingSlash(canonicalWebUrlProvider.get());
+  }
+}
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubLogin.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubLogin.java
index ece944d..02889a1 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubLogin.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubLogin.java
@@ -38,7 +38,9 @@
 import org.kohsuke.github.GHMyself;
 import org.kohsuke.github.GitHub;
 import org.kohsuke.github.GitHubBuilder;
-import org.kohsuke.github.HttpConnector;
+import org.kohsuke.github.HttpException;
+import org.kohsuke.github.connector.GitHubConnector;
+import org.kohsuke.github.internal.GitHubConnectorHttpConnectorAdapter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -63,7 +65,9 @@
 
   private SortedSet<Scope> loginScopes;
   private final GitHubOAuthConfig config;
-  private final HttpConnector httpConnector;
+  private final CanonicalWebUrls canonicalWebUrls;
+  private final VirtualDomainConfig virtualDomainConfig;
+  private final GitHubConnector gitHubConnector;
 
   public GHMyself getMyself() throws IOException {
     if (isLoggedIn()) {
@@ -72,17 +76,35 @@
     return null;
   }
 
-  public Set<String> getMyOrganisationsLogins() throws IOException {
+  public Set<String> getMyOrganisationsLogins(String username) throws IOException {
     if (isLoggedIn()) {
-      return getHub().getMyOrganizations().keySet();
+      try {
+        return getHub().getMyOrganizations().keySet();
+      } catch (HttpException httpException) {
+        if (!httpException.getMessage().contains("You need at least")) {
+          throw httpException;
+        }
+        log.info(
+            "Cannot access organizations for user '{}': falling back to list of public"
+                + " organisations",
+            username);
+
+        return getHub().getUserPublicOrganizations(username).keySet();
+      }
     }
     return Collections.emptySet();
   }
 
   @Inject
-  public GitHubLogin(GitHubOAuthConfig config, GitHubHttpConnector httpConnector) {
+  public GitHubLogin(
+      GitHubOAuthConfig config,
+      CanonicalWebUrls canonicalWebUrls,
+      VirtualDomainConfig virutalDomainConfig,
+      GitHubHttpConnector httpConnector) {
     this.config = config;
-    this.httpConnector = httpConnector;
+    this.canonicalWebUrls = canonicalWebUrls;
+    this.virtualDomainConfig = virutalDomainConfig;
+    this.gitHubConnector = GitHubConnectorHttpConnectorAdapter.adapt(httpConnector);
   }
 
   public boolean isLoggedIn() {
@@ -107,12 +129,13 @@
         response.sendRedirect(OAuthProtocol.getTargetUrl(request));
       }
     } else {
-      Set<ScopeKey> configuredScopesProfiles = config.scopes.keySet();
+      Set<ScopeKey> configuredScopesProfiles = virtualDomainConfig.getScopes(request).keySet();
       String scopeRequested = getScopesKey(request, response);
       if (Strings.isNullOrEmpty(scopeRequested) && configuredScopesProfiles.size() > 1) {
-        response.sendRedirect(config.getScopeSelectionUrl(request));
+        response.sendRedirect(canonicalWebUrls.getScopeSelectionUrl());
       } else {
-        this.loginScopes = getScopes(MoreObjects.firstNonNull(scopeRequested, "scopes"), scopes);
+        this.loginScopes =
+            getScopes(request, MoreObjects.firstNonNull(scopeRequested, "scopes"), scopes);
         log.debug("Login-PHASE1 " + this);
         state = oauth.loginPhase1(request, response, loginScopes);
       }
@@ -141,7 +164,7 @@
     return new GitHubBuilder()
         .withEndpoint(config.gitHubApiUrl)
         .withOAuthToken(token.accessToken)
-        .withConnector(httpConnector)
+        .withConnector(gitHubConnector)
         .build();
   }
 
@@ -178,15 +201,15 @@
     return null;
   }
 
-  private SortedSet<Scope> getScopes(String baseScopeKey, Scope... scopes) {
-    HashSet<Scope> fullScopes = new HashSet<>(scopesForKey(baseScopeKey));
+  private SortedSet<Scope> getScopes(HttpServletRequest req, String baseScopeKey, Scope... scopes) {
+    HashSet<Scope> fullScopes = new HashSet<>(scopesForKey(req, baseScopeKey));
     fullScopes.addAll(Arrays.asList(scopes));
 
     return new TreeSet<>(fullScopes);
   }
 
-  private List<Scope> scopesForKey(String baseScopeKey) {
-    return config.scopes.entrySet().stream()
+  private List<Scope> scopesForKey(HttpServletRequest req, String baseScopeKey) {
+    return virtualDomainConfig.getScopes(req).entrySet().stream()
         .filter(entry -> entry.getKey().name.equals(baseScopeKey))
         .map(entry -> entry.getValue())
         .findFirst()
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubOAuthConfig.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubOAuthConfig.java
index 19eebf2..300a945 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubOAuthConfig.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubOAuthConfig.java
@@ -13,14 +13,14 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.github.oauth;
 
+import static com.googlesource.gerrit.plugins.github.oauth.CanonicalWebUrls.trimTrailingSlash;
 import static com.googlesource.gerrit.plugins.github.oauth.GitHubOAuthConfig.KeyConfig.PASSWORD_DEVICE_CONFIG_LABEL;
 
-import com.google.common.base.CharMatcher;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSortedMap;
 import com.google.gerrit.extensions.client.AuthType;
-import com.google.gerrit.httpd.CanonicalWebUrl;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
@@ -35,17 +35,16 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.SortedMap;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 import java.util.stream.Collectors;
-import javax.servlet.http.HttpServletRequest;
 import lombok.Getter;
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
 public class GitHubOAuthConfig {
   private final Config config;
-  private final CanonicalWebUrl canonicalWebUrl;
 
   public static final String CONF_SECTION = "github";
   public static final String CONF_KEY_SECTION = "github-key";
@@ -67,10 +66,11 @@
   public final String httpHeader;
   public final String gitHubOAuthUrl;
   public final String gitHubOAuthAccessTokenUrl;
+  public final String scopeSelectionUrl;
   public final boolean enabled;
 
-  @Getter public final Map<ScopeKey, List<OAuthProtocol.Scope>> scopes;
-  @Getter public final List<ScopeKey> sortedScopesKeys;
+  @Getter public final SortedMap<ScopeKey, List<OAuthProtocol.Scope>> scopes;
+  @Getter public final Map<String, SortedMap<ScopeKey, List<OAuthProtocol.Scope>>> virtualScopes;
 
   public final int fileUpdateMaxRetryCount;
   public final int fileUpdateMaxRetryIntervalMsec;
@@ -83,9 +83,8 @@
   private final Optional<String> cookieDomain;
 
   @Inject
-  protected GitHubOAuthConfig(@GerritServerConfig Config config, CanonicalWebUrl canonicalWebUrl) {
+  protected GitHubOAuthConfig(@GerritServerConfig Config config) {
     this.config = config;
-    this.canonicalWebUrl = canonicalWebUrl;
 
     httpHeader =
         Preconditions.checkNotNull(
@@ -106,6 +105,7 @@
         Preconditions.checkNotNull(
             config.getString(CONF_SECTION, null, "clientSecret"),
             "GitHub `clientSecret` must be provided");
+    scopeSelectionUrl = config.getString(CONF_SECTION, null, "scopeSelectionUrl");
 
     oauthHttpHeader = config.getString("auth", null, "httpExternalIdHeader");
     gitHubOAuthUrl = gitHubUrl + GITHUB_OAUTH_AUTHORIZE;
@@ -115,10 +115,7 @@
     enabled = config.getString("auth", null, "type").equalsIgnoreCase(AuthType.HTTP.toString());
     cookieDomain = Optional.ofNullable(config.getString("auth", null, "cookieDomain"));
     scopes = getScopes(config);
-    sortedScopesKeys =
-        scopes.keySet().stream()
-            .sorted(Comparator.comparing(ScopeKey::getSequence))
-            .collect(Collectors.toList());
+    virtualScopes = getVirtualScopes(config);
 
     fileUpdateMaxRetryCount = config.getInt(CONF_SECTION, "fileUpdateMaxRetryCount", 3);
     fileUpdateMaxRetryIntervalMsec =
@@ -152,36 +149,29 @@
     currentKeyConfig = currentKeyConfigs.get(0);
   }
 
-  public String getOAuthFinalRedirectUrl(HttpServletRequest req) {
-    return req == null
-        ? GERRIT_OAUTH_FINAL
-        : trimTrailingSlash(canonicalWebUrl.get(req)) + GERRIT_OAUTH_FINAL;
+  private SortedMap<ScopeKey, List<Scope>> getScopes(Config config) {
+    return getScopesInSection(config, null);
   }
 
-  public String getScopeSelectionUrl(HttpServletRequest req) {
-    String canonicalUrl = req == null ? "" : trimTrailingSlash(canonicalWebUrl.get(req));
-    return canonicalUrl
-        + MoreObjects.firstNonNull(
-            config.getString(CONF_SECTION, null, "scopeSelectionUrl"), GITHUB_PLUGIN_OAUTH_SCOPE);
+  private Map<String, SortedMap<ScopeKey, List<Scope>>> getVirtualScopes(Config config) {
+    return config.getSubsections(CONF_SECTION).stream()
+        .collect(Collectors.toMap(k -> k, v -> getScopesInSection(config, v)));
   }
 
-  private Map<ScopeKey, List<Scope>> getScopes(Config config) {
-    return config.getNames(CONF_SECTION, true).stream()
+  private SortedMap<ScopeKey, List<Scope>> getScopesInSection(Config config, String subsection) {
+    return config.getNames(CONF_SECTION, subsection, true).stream()
         .filter(k -> k.startsWith("scopes"))
         .filter(k -> !k.endsWith("Description"))
         .filter(k -> !k.endsWith("Sequence"))
         .collect(
-            Collectors.toMap(
+            ImmutableSortedMap.toImmutableSortedMap(
+                Comparator.comparing(ScopeKey::getSequence),
                 k ->
                     new ScopeKey(
                         k,
-                        config.getString(CONF_SECTION, null, k + "Description"),
-                        config.getInt(CONF_SECTION, k + "Sequence", 0)),
-                v -> parseScopesString(config.getString(CONF_SECTION, null, v))));
-  }
-
-  private String trimTrailingSlash(String url) {
-    return CharMatcher.is('/').trimTrailingFrom(url);
+                        config.getString(CONF_SECTION, subsection, k + "Description"),
+                        config.getInt(CONF_SECTION, subsection, k + "Sequence", 0)),
+                v -> parseScopesString(config.getString(CONF_SECTION, subsection, v))));
   }
 
   private List<Scope> parseScopesString(String scopesString) {
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/IdentifiedUserGitHubLoginProvider.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/IdentifiedUserGitHubLoginProvider.java
index 5449347..63414b2 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/IdentifiedUserGitHubLoginProvider.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/IdentifiedUserGitHubLoginProvider.java
@@ -36,23 +36,20 @@
       ExternalId.SCHEME_EXTERNAL + ":" + OAuthWebFilter.GITHUB_EXT_ID;
 
   private final Provider<IdentifiedUser> userProvider;
-  private final GitHubOAuthConfig config;
   private final AccountCache accountCache;
-  private final GitHubHttpConnector httpConnector;
   private final OAuthTokenCipher oAuthTokenCipher;
+  private final Provider<GitHubLogin> gitHubLoginProvider;
 
   @Inject
   public IdentifiedUserGitHubLoginProvider(
+      Provider<GitHubLogin> gitHubLoginaprovider,
       Provider<IdentifiedUser> identifiedUserProvider,
-      GitHubOAuthConfig config,
-      GitHubHttpConnector httpConnector,
       AccountCache accountCache,
       OAuthTokenCipher oAuthTokenCipher) {
     this.userProvider = identifiedUserProvider;
-    this.config = config;
     this.accountCache = accountCache;
-    this.httpConnector = httpConnector;
     this.oAuthTokenCipher = oAuthTokenCipher;
+    this.gitHubLoginProvider = gitHubLoginaprovider;
   }
 
   @Override
@@ -67,7 +64,7 @@
     try {
       AccessToken accessToken = newAccessTokenFromUser(username);
       if (accessToken != null) {
-        GitHubLogin login = new GitHubLogin(config, httpConnector);
+        GitHubLogin login = gitHubLoginProvider.get();
         login.login(accessToken);
         return login;
       }
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthProtocol.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthProtocol.java
index b93837b..035f1ce 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthProtocol.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthProtocol.java
@@ -155,6 +155,7 @@
   private static SecureRandom randomState = newRandomGenerator();
 
   private final GitHubOAuthConfig config;
+  private final CanonicalWebUrls canonicalWebUrls;
   private final Gson gson;
   private final Provider<HttpClient> httpProvider;
 
@@ -231,6 +232,7 @@
   @Inject
   public OAuthProtocol(
       GitHubOAuthConfig config,
+      CanonicalWebUrls canonicalWebUrls,
       PooledHttpClientProvider httpClientProvider,
       /*
        * We need to explicitly tell Guice which Provider<> we need as this class
@@ -239,6 +241,7 @@
        */
       GsonProvider gsonProvider) {
     this.config = config;
+    this.canonicalWebUrls = canonicalWebUrls;
     this.httpProvider = httpClientProvider;
     this.gson = gsonProvider.get();
   }
@@ -256,7 +259,7 @@
         + "?client_id="
         + config.gitHubClientId
         + getURLEncodedParameter("&scope=", scopesString)
-        + getURLEncodedParameter("&redirect_uri=", config.getOAuthFinalRedirectUrl(req))
+        + getURLEncodedParameter("&redirect_uri=", canonicalWebUrls.getOAuthFinalRedirectUrl())
         + getURLEncodedParameter("&state=", state);
   }
 
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthWebFilter.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthWebFilter.java
index 0bca570..20b80bd 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthWebFilter.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthWebFilter.java
@@ -131,7 +131,7 @@
       String user = myself.getLogin();
 
       updateSecureConfigWithRetry(
-          ghLogin.getHub().getMyOrganizations().keySet(), user, ghLogin.getToken().accessToken);
+          ghLogin.getMyOrganisationsLogins(user), user, ghLogin.getToken().accessToken);
     }
   }
 
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/VirtualDomainConfig.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/VirtualDomainConfig.java
new file mode 100644
index 0000000..a6b2b17
--- /dev/null
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/VirtualDomainConfig.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2023 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.github.oauth;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.List;
+import java.util.Optional;
+import java.util.SortedMap;
+import javax.servlet.http.HttpServletRequest;
+
+@Singleton
+public class VirtualDomainConfig {
+  private final GitHubOAuthConfig oauthConfig;
+
+  @Inject
+  VirtualDomainConfig(GitHubOAuthConfig oauthConfig) {
+    this.oauthConfig = oauthConfig;
+  }
+
+  public SortedMap<ScopeKey, List<OAuthProtocol.Scope>> getScopes(HttpServletRequest req) {
+    String serverName = req.getServerName();
+    return Optional.ofNullable(oauthConfig.virtualScopes.get(serverName))
+        .orElse(oauthConfig.scopes);
+  }
+}
diff --git a/github-oauth/src/test/java/com/googlesource/gerrit/plugins/github/oauth/GitHubOAuthConfigTest.java b/github-oauth/src/test/java/com/googlesource/gerrit/plugins/github/oauth/GitHubOAuthConfigTest.java
index 6e18170..7a11427 100644
--- a/github-oauth/src/test/java/com/googlesource/gerrit/plugins/github/oauth/GitHubOAuthConfigTest.java
+++ b/github-oauth/src/test/java/com/googlesource/gerrit/plugins/github/oauth/GitHubOAuthConfigTest.java
@@ -22,20 +22,20 @@
 import static com.googlesource.gerrit.plugins.github.oauth.GitHubOAuthConfig.KeyConfig.SECRET_KEY_CONFIG_LABEL;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 
 import com.google.gerrit.extensions.client.AuthType;
-import com.google.gerrit.httpd.CanonicalWebUrl;
-import com.google.inject.AbstractModule;
-import com.google.inject.Guice;
-import com.google.inject.util.Providers;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
 import java.util.Optional;
+import java.util.SortedMap;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
 
 public class GitHubOAuthConfigTest {
 
-  CanonicalWebUrl canonicalWebUrl;
   Config config;
   private static final String testPasswordDevice = "/dev/zero";
 
@@ -46,18 +46,6 @@
     config.setString(CONF_SECTION, null, "clientId", "theClientId");
     config.setString("auth", null, "httpHeader", "GITHUB_USER");
     config.setString("auth", null, "type", AuthType.HTTP.toString());
-
-    canonicalWebUrl =
-        Guice.createInjector(
-                new AbstractModule() {
-                  @Override
-                  protected void configure() {
-                    bind(String.class)
-                        .annotatedWith(com.google.gerrit.server.config.CanonicalWebUrl.class)
-                        .toProvider(Providers.of(null));
-                  }
-                })
-            .getInstance(CanonicalWebUrl.class);
   }
 
   @Test
@@ -180,8 +168,46 @@
     assertEquals(Optional.of(myDomain), githubOAuthConfig().getCookieDomain());
   }
 
+  @Test
+  public void shouldReturnOverridesForSpecificHostName() {
+    setupEncryptionConfig();
+    String vhost = "v.host.com";
+    String scope1Name = "scopesRepo";
+    String scope1Description = "repo scope description";
+    String scope2Name = "scopesVHost";
+    String scope2Description = "scope description";
+
+    // virtual host scopes
+    config.setString(CONF_SECTION, vhost, scope2Name, "USER_EMAIL");
+    config.setInt(CONF_SECTION, vhost, scope2Name + "Sequence", 1);
+    config.setString(CONF_SECTION, vhost, scope2Name + "Description", scope2Description);
+    config.setString(CONF_SECTION, vhost, scope1Name, "REPO");
+    config.setInt(CONF_SECTION, vhost, scope1Name + "Sequence", 0);
+    config.setString(CONF_SECTION, vhost, scope1Name + "Description", scope1Description);
+
+    Map<String, SortedMap<ScopeKey, List<OAuthProtocol.Scope>>> virtualScopes =
+        githubOAuthConfig().getVirtualScopes();
+
+    assertTrue(virtualScopes.containsKey(vhost));
+
+    SortedMap<ScopeKey, List<OAuthProtocol.Scope>> vhostConfig = virtualScopes.get(vhost);
+    List<Map.Entry<ScopeKey, List<OAuthProtocol.Scope>>> entries =
+        new ArrayList<>(vhostConfig.entrySet());
+    Map.Entry<ScopeKey, List<OAuthProtocol.Scope>> firstEntry = entries.get(0);
+    Map.Entry<ScopeKey, List<OAuthProtocol.Scope>> secondEntry = entries.get(1);
+
+    assertEquals(firstEntry.getKey().name, scope1Name);
+    assertEquals(firstEntry.getKey().description, scope1Description);
+    assertEquals(firstEntry.getKey().sequence, 0);
+    assertEquals(List.of(OAuthProtocol.Scope.REPO), firstEntry.getValue());
+    assertEquals(secondEntry.getKey().name, scope2Name);
+    assertEquals(secondEntry.getKey().description, scope2Description);
+    assertEquals(secondEntry.getKey().sequence, 1);
+    assertEquals(List.of(OAuthProtocol.Scope.USER_EMAIL), secondEntry.getValue());
+  }
+
   private GitHubOAuthConfig githubOAuthConfig() {
-    return new GitHubOAuthConfig(config, canonicalWebUrl);
+    return new GitHubOAuthConfig(config);
   }
 
   private void setupEncryptionConfig() {
diff --git a/github-oauth/src/test/java/com/googlesource/gerrit/plugins/github/oauth/OAuthTokenCipherTest.java b/github-oauth/src/test/java/com/googlesource/gerrit/plugins/github/oauth/OAuthTokenCipherTest.java
index 3e31d6b..f3dfb71 100644
--- a/github-oauth/src/test/java/com/googlesource/gerrit/plugins/github/oauth/OAuthTokenCipherTest.java
+++ b/github-oauth/src/test/java/com/googlesource/gerrit/plugins/github/oauth/OAuthTokenCipherTest.java
@@ -26,10 +26,6 @@
 import static org.junit.Assert.assertThrows;
 
 import com.google.gerrit.extensions.client.AuthType;
-import com.google.gerrit.httpd.CanonicalWebUrl;
-import com.google.inject.AbstractModule;
-import com.google.inject.Guice;
-import com.google.inject.util.Providers;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
@@ -43,7 +39,6 @@
 
 public class OAuthTokenCipherTest {
 
-  CanonicalWebUrl canonicalWebUrl;
   Config config;
 
   @ClassRule public static TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -63,18 +58,6 @@
         CONF_KEY_SECTION, VERSION1_KEY_ID, PASSWORD_DEVICE_CONFIG_LABEL, testPasswordDevice);
     config.setString(
         CONF_KEY_SECTION, VERSION2_KEY_ID, PASSWORD_DEVICE_CONFIG_LABEL, testPasswordDevice);
-
-    canonicalWebUrl =
-        Guice.createInjector(
-                new AbstractModule() {
-                  @Override
-                  protected void configure() {
-                    bind(String.class)
-                        .annotatedWith(com.google.gerrit.server.config.CanonicalWebUrl.class)
-                        .toProvider(Providers.of(null));
-                  }
-                })
-            .getInstance(CanonicalWebUrl.class);
   }
 
   @Test
@@ -193,7 +176,7 @@
   }
 
   private OAuthTokenCipher objectUnderTest(Config testConfig) throws IOException {
-    return new OAuthTokenCipher(new GitHubOAuthConfig(testConfig, canonicalWebUrl));
+    return new OAuthTokenCipher(new GitHubOAuthConfig(testConfig));
   }
 
   private static Config createCommonConfig() {
diff --git a/github-plugin/.gitignore b/github-plugin/.gitignore
index 80d6257..1080f67 100644
--- a/github-plugin/.gitignore
+++ b/github-plugin/.gitignore
@@ -3,3 +3,6 @@
 /.project
 /.settings/org.maven.ide.eclipse.prefs
 /.settings/org.eclipse.m2e.core.prefs
+/node_modules
+yarn-error.log
+/.rollup.cache
diff --git a/github-plugin/package.json b/github-plugin/package.json
new file mode 100644
index 0000000..209cc44
--- /dev/null
+++ b/github-plugin/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "github-oauth-ui",
+  "description": "UI for the Gerrit GitHub OAuth plugin",
+  "browser": true,
+  "dependencies": {
+    "@gerritcodereview/typescript-api": "^3.8.0",
+    "@lit/ts-transformers": "^1.1.3",
+    "@polymer/polymer": "^3.5.1",
+    "@rollup/plugin-node-resolve": "^15.2.1",
+    "@rollup/plugin-terser": "^0.4.3",
+    "lit": "^2.8.0",
+    "rollup": "^3.29.4",
+    "typescript": "^4.9.5"
+  },
+  "license": "Apache-2.0",
+  "private": true,
+  "scripts": {
+    "build": "tsc && rollup -c"
+  }
+}
diff --git a/github-plugin/pom.xml b/github-plugin/pom.xml
index b77cd4e..1eec4f3 100644
--- a/github-plugin/pom.xml
+++ b/github-plugin/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <artifactId>github-parent</artifactId>
     <groupId>com.googlesource.gerrit.plugins.github</groupId>
-    <version>3.8.0</version>
+    <version>3.9.0-rc5</version>
   </parent>
 
   <artifactId>github-plugin</artifactId>
@@ -87,6 +87,57 @@
             <goals>
               <goal>shade</goal>
             </goals>
+            <configuration>
+              <filters>
+                <filter>
+                  <!-- is a signed jar hence the singature has to be removed during shading -->
+                  <artifact>org.eclipse.mylyn.github:org.eclipse.egit.github.core</artifact>
+                  <excludes>
+                    <exclude>META-INF/*.SF</exclude>
+                    <exclude>META-INF/*.DSA</exclude>
+                    <exclude>META-INF/*.RSA</exclude>
+                  </excludes>
+                </filter>
+              </filters>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>com.github.eirslett</groupId>
+        <artifactId>frontend-maven-plugin</artifactId>
+        <version>1.14.0</version>
+        <configuration>
+          <installDirectory>target</installDirectory>
+        </configuration>
+        <executions>
+          <execution>
+            <id>install node and yarn</id>
+            <goals>
+              <goal>install-node-and-yarn</goal>
+            </goals>
+            <configuration>
+              <nodeVersion>v17.9.1</nodeVersion>
+              <yarnVersion>v1.22.19</yarnVersion>
+            </configuration>
+          </execution>
+          <execution>
+            <id>yarn install</id>
+            <goals>
+              <goal>yarn</goal>
+            </goals>
+            <configuration>
+              <arguments>install</arguments>
+            </configuration>
+          </execution>
+          <execution>
+            <id>yarn build</id>
+            <goals>
+              <goal>yarn</goal>
+            </goals>
+            <configuration>
+              <arguments>build</arguments>
+            </configuration>
           </execution>
         </executions>
       </plugin>
@@ -101,6 +152,12 @@
       <scope>provided</scope>
     </dependency>
     <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-acceptance-framework</artifactId>
+      <version>${Gerrit-ApiVersion}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
       <groupId>${project.groupId}</groupId>
       <artifactId>github-oauth</artifactId>
       <version>${project.version}</version>
@@ -108,7 +165,7 @@
     <dependency>
       <groupId>javax.servlet</groupId>
       <artifactId>javax.servlet-api</artifactId>
-      <version>3.0.1</version>
+      <version>3.1.0</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
@@ -124,33 +181,33 @@
     <dependency>
       <groupId>org.eclipse.mylyn.github</groupId>
       <artifactId>org.eclipse.egit.github.core</artifactId>
-      <version>1.3.1</version>
+      <version>6.1.0.202203080745-r</version>
     </dependency>
     <dependency>
       <groupId>com.google.code.gson</groupId>
       <artifactId>gson</artifactId>
-      <version>2.8.6</version>
+      <version>2.10.1</version>
     </dependency>
     <dependency>
       <groupId>org.apache.httpcomponents</groupId>
       <artifactId>httpclient</artifactId>
-      <version>4.4</version>
+      <version>4.5.2</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
-      <groupId>javax.mail</groupId>
-      <artifactId>mail</artifactId>
-      <version>1.4.5-rc1</version>
+      <groupId>com.sun.mail</groupId>
+      <artifactId>javax.mail</artifactId>
+      <version>1.6.2</version>
     </dependency>
     <dependency>
-      <groupId>org.apache.commons</groupId>
+      <groupId>commons-io</groupId>
       <artifactId>commons-io</artifactId>
-      <version>1.3.2</version>
+      <version>2.14.0</version>
     </dependency>
     <dependency>
       <groupId>commons-discovery</groupId>
       <artifactId>commons-discovery</artifactId>
-      <version>20040218.194635</version>
+      <version>0.5</version>
     </dependency>
     <dependency>
       <groupId>org.apache.velocity</groupId>
diff --git a/github-plugin/rollup.config.mjs b/github-plugin/rollup.config.mjs
new file mode 100644
index 0000000..03d92be
--- /dev/null
+++ b/github-plugin/rollup.config.mjs
@@ -0,0 +1,17 @@
+import terser from '@rollup/plugin-terser';
+import { nodeResolve } from '@rollup/plugin-node-resolve';
+
+export default {
+  input: 'target/web/src/main/ts/main.js',
+  treeshake: false,
+  output: {
+    format: 'iife',
+    compact: true,
+    file: 'target/classes/static/github-plugin.js',
+  },
+  context: 'window',
+  plugins: [
+    terser(),
+    nodeResolve(),
+  ],
+}
\ No newline at end of file
diff --git a/github-plugin/src/main/java/com/google/gerrit/server/account/AccountImporter.java b/github-plugin/src/main/java/com/google/gerrit/server/account/AccountImporter.java
index e826880..1c0fe3b 100644
--- a/github-plugin/src/main/java/com/google/gerrit/server/account/AccountImporter.java
+++ b/github-plugin/src/main/java/com/google/gerrit/server/account/AccountImporter.java
@@ -15,10 +15,10 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdFactory;
-import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GitHubConfig.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GitHubConfig.java
index a22126d..2bacafd 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GitHubConfig.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GitHubConfig.java
@@ -17,7 +17,6 @@
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.Table;
 import com.google.gerrit.entities.Account;
-import com.google.gerrit.httpd.CanonicalWebUrl;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
@@ -75,10 +74,9 @@
   public GitHubConfig(
       @GerritServerConfig Config config,
       final SitePaths site,
-      Provider<AllProjectsName> allProjectsNameProvider,
-      CanonicalWebUrl canonicalWebUrl)
+      Provider<AllProjectsName> allProjectsNameProvider)
       throws MalformedURLException {
-    super(config, canonicalWebUrl);
+    super(config);
     parseWizardFlow(config.getStringList(CONF_SECTION, null, CONF_WIZARD_FLOW), DEFAULT_SERVER);
 
     // Virtual host specific sections
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GitHubTopMenu.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GitHubTopMenu.java
index 5ff5363..d345556 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GitHubTopMenu.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GitHubTopMenu.java
@@ -47,7 +47,7 @@
             new MenuEntry(
                 "GitHub",
                 Arrays.asList(
-                    getItem("Scope", ghConfig.getScopeSelectionUrl(null)),
+                    getItem("Scope", baseUrl + "/static/scope.html"),
                     getItem("Profile", baseUrl + "/static/account.html"),
                     getItem("Repositories", baseUrl + "/static/repositories.html"),
                     getItem("Pull Requests", baseUrl + "/static/pullrequests.html"))));
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceHttpModule.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceHttpModule.java
index f75030d..ef24191 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceHttpModule.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceHttpModule.java
@@ -15,10 +15,16 @@
 
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.webui.JavaScriptPlugin;
+import com.google.gerrit.extensions.webui.WebUiPlugin;
+import com.google.gerrit.httpd.AllRequestFilter;
+import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
 import com.google.inject.name.Names;
 import com.google.inject.servlet.ServletModule;
+import com.googlesource.gerrit.plugins.github.filters.GitHubGroupCacheRefreshFilter;
 import com.googlesource.gerrit.plugins.github.filters.GitHubOAuthFilter;
 import com.googlesource.gerrit.plugins.github.git.CreateProjectStep;
 import com.googlesource.gerrit.plugins.github.git.GitCloneStep;
@@ -90,6 +96,9 @@
         .annotatedWith(Exports.named("github"))
         .to(GitHubOAuthServiceProvider.class);
 
+    DynamicSet.bind(binder(), WebUiPlugin.class)
+        .toInstance(new JavaScriptPlugin("github-plugin.js"));
+
     serve("*.css", "*.js", "*.png", "*.jpg", "*.woff", "*.gif", "*.ttf")
         .with(VelocityStaticServlet.class);
     serve("*.gh").with(VelocityControllerServlet.class);
@@ -97,5 +106,9 @@
 
     serve("/static/*").with(VelocityViewServlet.class);
     filterRegex("(?!/webhook).*").through(GitHubOAuthFilter.class);
+
+    DynamicSet.bind(binder(), AllRequestFilter.class)
+        .to(GitHubGroupCacheRefreshFilter.class)
+        .in(Scopes.SINGLETON);
   }
 }
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/filters/GitHubGroupCacheRefreshFilter.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/filters/GitHubGroupCacheRefreshFilter.java
new file mode 100644
index 0000000..eb34962
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/filters/GitHubGroupCacheRefreshFilter.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2023 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.github.filters;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.httpd.AllRequestFilter;
+import com.googlesource.gerrit.plugins.github.group.GitHubGroupsCache;
+import java.io.IOException;
+import java.util.Optional;
+import javax.inject.Inject;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class GitHubGroupCacheRefreshFilter extends AllRequestFilter {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final String LOGIN_URL = "/login";
+  private static final String LOGIN_QUERY_FINAL = "final=true";
+  private static final String ACCOUNT_COOKIE = "GerritAccount";
+  private static final String INVALIDATE_CACHED_GROUPS = "RefreshGroups";
+
+  private final GitHubGroupsCache ghGroupsCache;
+
+  @Inject
+  @VisibleForTesting
+  public GitHubGroupCacheRefreshFilter(GitHubGroupsCache ghGroupsCache) {
+    this.ghGroupsCache = ghGroupsCache;
+  }
+
+  @Override
+  public void init(FilterConfig filterConfig) throws ServletException {}
+
+  @Override
+  public void doFilter(
+      ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
+      throws IOException, ServletException {
+    filterChain.doFilter(servletRequest, servletResponse);
+
+    HttpServletRequest req = (HttpServletRequest) servletRequest;
+    if (req.getRequestURI().endsWith(LOGIN_URL) && req.getQueryString().equals(LOGIN_QUERY_FINAL)) {
+      HttpServletResponse resp = (HttpServletResponse) servletResponse;
+      String cookieResponse = resp.getHeader("Set-Cookie");
+      if (cookieResponse != null && cookieResponse.contains(ACCOUNT_COOKIE)) {
+        req.getSession().setAttribute(INVALIDATE_CACHED_GROUPS, Boolean.TRUE);
+      }
+    } else if (hasSessionFlagForInvalidatingCachedUserGroups(req)) {
+      ghGroupsCache.invalidateCurrentUserGroups();
+      req.getSession().removeAttribute(INVALIDATE_CACHED_GROUPS);
+    }
+  }
+
+  private static boolean hasSessionFlagForInvalidatingCachedUserGroups(HttpServletRequest req) {
+    return Optional.ofNullable(req.getSession(false))
+        .flatMap(session -> Optional.ofNullable(session.getAttribute(INVALIDATE_CACHED_GROUPS)))
+        .filter(refresh -> (Boolean) refresh)
+        .isPresent();
+  }
+
+  @Override
+  public void destroy() {}
+}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/BatchImporter.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/BatchImporter.java
index f26188d..d359526 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/BatchImporter.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/BatchImporter.java
@@ -44,7 +44,7 @@
   }
 
   public synchronized void schedule(int idx, GitJob pullRequestImportJob) {
-    jobs.put(new Integer(idx), pullRequestImportJob);
+    jobs.put(Integer.valueOf(idx), pullRequestImportJob);
     executor.exec(pullRequestImportJob);
   }
 }
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/FanoutReplicationConfig.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/FanoutReplicationConfig.java
index 412c2ff..4a3bc4b 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/FanoutReplicationConfig.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/FanoutReplicationConfig.java
@@ -54,12 +54,19 @@
             FS.DETECTED);
 
     replicationConf.load();
-    replicationConf.setString("remote", null, "url", url);
+    String currentUrl = replicationConf.getString("remote", null, "url");
+    if (currentUrl == null) {
+      replicationConf.setString("remote", null, "url", url);
+    }
     List<String> projects =
         new ArrayList<>(Arrays.asList(replicationConf.getStringList("remote", null, "projects")));
     projects.add(projectName);
     replicationConf.setStringList("remote", null, "projects", projects);
-    replicationConf.setString("remote", null, "push", "refs/*:refs/*");
+
+    String currentPushRefs = replicationConf.getString("remote", null, "push");
+    if (currentPushRefs == null) {
+      replicationConf.setString("remote", null, "push", "refs/*:refs/*");
+    }
     replicationConf.save();
   }
 }
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/GitCloneStep.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/GitCloneStep.java
index 12e5101..9d09f51 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/GitCloneStep.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/GitCloneStep.java
@@ -13,6 +13,8 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.github.git;
 
+import static java.util.stream.Collectors.toList;
+
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -34,6 +36,7 @@
 import com.googlesource.gerrit.plugins.github.GitHubConfig;
 import java.io.File;
 import java.io.IOException;
+import java.util.stream.Stream;
 import org.apache.commons.io.FileUtils;
 import org.eclipse.jgit.api.FetchCommand;
 import org.eclipse.jgit.api.Git;
@@ -50,11 +53,8 @@
   private static final Logger LOG = LoggerFactory.getLogger(GitImporter.class);
 
   private final GitHubConfig config;
-  private final File gitDir;
   private final GerritApi gerritApi;
   private final OneOffRequestContext context;
-  private final String organisation;
-  private final String repository;
   private final File destinationDirectory;
   private final DynamicSet<ProjectDeletedListener> deletedListeners;
   private final ProjectCache projectCache;
@@ -85,14 +85,11 @@
     super(config.gitHubUrl, organisation, repository, gitHubRepoFactory);
     LOG.debug("GitHub Clone " + organisation + "/" + repository);
     this.config = config;
-    this.gitDir = config.gitDir.toFile();
 
     this.gerritApi = gerritApi;
     this.context = context;
-    this.organisation = organisation;
-    this.repository = repository;
     this.projectName = organisation + "/" + repository;
-    this.destinationDirectory = prepareTargetGitDirectory(gitDir, this.projectName);
+    this.destinationDirectory = prepareTargetGitDirectory(config.gitDir.toFile(), this.projectName);
     this.deletedListeners = deletedListeners;
     this.projectCache = projectCache;
     this.repoManager = repoManager;
@@ -113,7 +110,9 @@
     try (ManualRequestContext requestContext = context.openAs(config.importAccountId)) {
       ProjectInput pi = new ProjectInput();
       pi.name = projectName;
-      pi.parent = config.getBaseProject(getRepository().isPrivate());
+      GitHubRepository ghRepository = getRepository();
+      pi.parent = config.getBaseProject(ghRepository.isPrivate());
+      pi.branches = Stream.ofNullable(ghRepository.getDefaultBranch()).collect(toList());
       gerritApi.projects().create(pi).get();
     } catch (ResourceConflictException e) {
       throw new GitDestinationAlreadyExistsException(projectName);
@@ -128,7 +127,8 @@
     Project.NameKey key = Project.nameKey(projectName);
     String sourceUri = getSourceUri();
     try (Git git = Git.open(destinationDirectory)) {
-      FetchCommand fetch = git.fetch().setRefSpecs("refs/*:refs/*").setRemote(sourceUri);
+      FetchCommand fetch =
+          git.fetch().setRefSpecs("^refs/changes/*", "refs/*:refs/*").setRemote(sourceUri);
       fetch.setCredentialsProvider(getRepository().getCredentialsProvider());
       if (progress != null) {
         fetch.setProgressMonitor(progress);
@@ -146,10 +146,6 @@
     }
   }
 
-  private boolean isNotEmpty(File destDirectory) {
-    return destDirectory.listFiles().length > 0;
-  }
-
   @Override
   public boolean rollback() {
     File gitDirectory = destinationDirectory;
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/MagicRefFoundException.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/MagicRefFoundException.java
index 263ee9d..42b7e36 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/MagicRefFoundException.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/MagicRefFoundException.java
@@ -15,6 +15,8 @@
 
 public class MagicRefFoundException extends GitException {
 
+  private static final long serialVersionUID = 1L;
+
   public MagicRefFoundException(String message) {
     super(message);
   }
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/ProtectedBranchFoundException.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/ProtectedBranchFoundException.java
index 221152f..c156150 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/ProtectedBranchFoundException.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/ProtectedBranchFoundException.java
@@ -16,6 +16,8 @@
 
 public class ProtectedBranchFoundException extends Exception {
 
+  private static final long serialVersionUID = 1L;
+
   public ProtectedBranchFoundException(String msg) {
     super(msg);
   }
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/PullRequestCreateChange.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/PullRequestCreateChange.java
index f2a7f06..810acd8 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/PullRequestCreateChange.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/PullRequestCreateChange.java
@@ -27,10 +27,10 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/PullRequestImportJob.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/PullRequestImportJob.java
index 52d7b34..da16dc2 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/PullRequestImportJob.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/PullRequestImportJob.java
@@ -191,7 +191,7 @@
                   pullRequestOwner,
                   revCommit,
                   getChangeMessage(pr),
-                  String.format(TOPIC_FORMAT, new Integer(pr.getNumber())));
+                  String.format(TOPIC_FORMAT, Integer.valueOf(pr.getNumber())));
           if (changeId != null) {
             prChanges.add(changeId);
           }
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/CurrentUsernameProvider.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/CurrentUsernameProvider.java
new file mode 100644
index 0000000..028aa6b
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/CurrentUsernameProvider.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2023 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.github.group;
+
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.Optional;
+
+public class CurrentUsernameProvider implements Provider<String> {
+  public static final String CURRENT_USERNAME = "CurrentUsername";
+
+  private final Provider<CurrentUser> userProvider;
+
+  @Inject
+  CurrentUsernameProvider(Provider<CurrentUser> userProvider) {
+    this.userProvider = userProvider;
+  }
+
+  @Override
+  public String get() {
+    return Optional.ofNullable(userProvider.get())
+        .filter(CurrentUser::isIdentifiedUser)
+        .map(CurrentUser::asIdentifiedUser)
+        .flatMap(IdentifiedUser::getUserName)
+        .orElse(null);
+  }
+}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupsCache.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupsCache.java
index 3504f2c..8f9c776 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupsCache.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupsCache.java
@@ -14,19 +14,21 @@
 
 package com.googlesource.gerrit.plugins.github.group;
 
+import static com.googlesource.gerrit.plugins.github.group.CurrentUsernameProvider.CURRENT_USERNAME;
 import static java.time.temporal.ChronoUnit.MINUTES;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.AccountGroup.UUID;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
+import com.google.inject.name.Names;
 import com.googlesource.gerrit.plugins.github.groups.OrganizationStructure;
 import com.googlesource.gerrit.plugins.github.oauth.GitHubLogin;
 import com.googlesource.gerrit.plugins.github.oauth.UserScopedProvider;
@@ -95,7 +97,7 @@
     private void loadOrganisations(
         String username, OrganizationStructure orgsTeams, GitHubLogin ghLogin) throws IOException {
       logger.debug("Getting list of public organisations for user '{}'", username);
-      Set<String> organisations = ghLogin.getMyOrganisationsLogins();
+      Set<String> organisations = ghLogin.getMyOrganisationsLogins(username);
       for (String org : organisations) {
         orgsTeams.put(org, EVERYONE_TEAM_NAME);
       }
@@ -106,6 +108,9 @@
     return new CacheModule() {
       @Override
       protected void configure() {
+        bind(String.class)
+            .annotatedWith(Names.named(CurrentUsernameProvider.CURRENT_USERNAME))
+            .toProvider(CurrentUsernameProvider.class);
         persist(ORGS_CACHE_NAME, String.class, OrganizationStructure.class)
             .expireAfterWrite(Duration.of(GROUPS_CACHE_TTL_MINS, MINUTES))
             .loader(OrganisationLoader.class);
@@ -115,14 +120,15 @@
   }
 
   private final LoadingCache<String, OrganizationStructure> orgTeamsByUsername;
-  private final Provider<IdentifiedUser> userProvider;
+  private final Provider<String> usernameProvider;
 
   @Inject
-  GitHubGroupsCache(
+  @VisibleForTesting
+  public GitHubGroupsCache(
       @Named(ORGS_CACHE_NAME) LoadingCache<String, OrganizationStructure> byUsername,
-      Provider<IdentifiedUser> userProvider) {
+      @Named(CURRENT_USERNAME) Provider<String> usernameProvider) {
     this.orgTeamsByUsername = byUsername;
-    this.userProvider = userProvider;
+    this.usernameProvider = usernameProvider;
   }
 
   Set<String> getOrganizationsForUser(String username) {
@@ -135,7 +141,7 @@
   }
 
   Set<String> getOrganizationsForCurrentUser() throws ExecutionException {
-    return orgTeamsByUsername.get(userProvider.get().getUserName().get()).keySet();
+    return orgTeamsByUsername.get(usernameProvider.get()).keySet();
   }
 
   Set<String> getTeamsForUser(String organizationName, String username) {
@@ -156,7 +162,7 @@
   }
 
   Set<String> getTeamsForCurrentUser(String organizationName) {
-    return getTeamsForUser(organizationName, userProvider.get().getUserName().get());
+    return getTeamsForUser(organizationName, usernameProvider.get());
   }
 
   public Set<UUID> getGroupsForUser(String username) {
@@ -170,4 +176,8 @@
     }
     return groupsBuilder.build();
   }
+
+  public void invalidateCurrentUserGroups() {
+    orgTeamsByUsername.invalidate(usernameProvider.get());
+  }
 }
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/notification/PullRequestHandler.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/notification/PullRequestHandler.java
index 540b98a..361ccc2 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/notification/PullRequestHandler.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/notification/PullRequestHandler.java
@@ -46,7 +46,7 @@
     String action = payload.getAction();
     if (action.equals("opened") || action.equals("synchronize")) {
       GHRepository repository = payload.getRepository();
-      Integer prNumber = new Integer(payload.getNumber());
+      Integer prNumber = Integer.valueOf(payload.getNumber());
       PullRequestImporter prImporter = prImportProvider.get();
       String organization = repository.getOwnerName();
       String name = repository.getName();
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/GitHubDestinations.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/GitHubDestinations.java
index af644fe..810632a 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/GitHubDestinations.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/GitHubDestinations.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
-import com.google.inject.Injector;
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.nio.file.Files;
@@ -52,24 +51,20 @@
     return null;
   }
 
-  private final Injector injector;
   private final List<Destination> configs;
 
   private final RemoteSiteUser.Factory replicationUserFactory;
   private final PluginUser pluginUser;
   private final GroupBackend groupBackend;
-  boolean replicateAllOnPluginStart;
   private final List<String> organisations;
 
   @Inject
   GitHubDestinations(
-      final Injector i,
       final SitePaths site,
       final RemoteSiteUser.Factory ruf,
       final GroupBackend gb,
       final PluginUser pu)
       throws ConfigInvalidException, IOException {
-    injector = i;
     pluginUser = pu;
     replicationUserFactory = ruf;
     groupBackend = gb;
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/velocity/PluginVelocityRuntimeProvider.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/velocity/PluginVelocityRuntimeProvider.java
index 144e77a..735ac4d 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/velocity/PluginVelocityRuntimeProvider.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/velocity/PluginVelocityRuntimeProvider.java
@@ -30,12 +30,12 @@
 
 @Singleton
 public class PluginVelocityRuntimeProvider implements Provider<RuntimeInstance> {
-  private static final String VELOCITY_FILE_RESOURCE_LOADER_PATH = "file.resource.loader.path";
-  private static final String VELOCITY_FILE_RESOURCE_LOADER_CLASS = "file.resource.loader.class";
-  private static final String VELOCITY_CLASS_RESOURCE_LOADER_CLASS = "class.resource.loader.class";
-  private static final String VELOCITY_JAR_RESOURCE_LOADER_CLASS = "jar.resource.loader.class";
-  private static final String VELOCITY_JAR_RESOURCE_LOADER_PATH = "jar.resource.loader.path";
-  private static final String VELOCITY_RESOURCE_LOADER = "resource.loader";
+  private static final String VELOCITY_FILE_RESOURCE_LOADER_PATH = "resource.loader.file.path";
+  private static final String VELOCITY_FILE_RESOURCE_LOADER_CLASS = "resource.loader.jar.path";
+  private static final String VELOCITY_CLASS_RESOURCE_LOADER_CLASS = "resource.loader.class.class";
+  private static final String VELOCITY_JAR_RESOURCE_LOADER_CLASS = "resource.loader.jar.class";
+  private static final String VELOCITY_JAR_RESOURCE_LOADER_PATH = "resource.loader.jar.path";
+  private static final String VELOCITY_RESOURCE_LOADER = "resource.loaders";
   private final SitePaths site;
   private String pluginName;
 
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/velocity/VelocityViewServlet.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/velocity/VelocityViewServlet.java
index 85197dd..5e4498f 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/velocity/VelocityViewServlet.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/velocity/VelocityViewServlet.java
@@ -13,15 +13,20 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.github.velocity;
 
+import static com.googlesource.gerrit.plugins.github.oauth.GitHubOAuthConfig.GITHUB_PLUGIN_OAUTH_SCOPE;
+
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
 import com.googlesource.gerrit.plugins.github.GitHubConfig;
+import com.googlesource.gerrit.plugins.github.oauth.CanonicalWebUrls;
 import com.googlesource.gerrit.plugins.github.oauth.GitHubLogin;
 import com.googlesource.gerrit.plugins.github.oauth.ScopedProvider;
+import com.googlesource.gerrit.plugins.github.oauth.VirtualDomainConfig;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.Map.Entry;
@@ -49,6 +54,9 @@
   private final ScopedProvider<GitHubLogin> loginProvider;
   private final Provider<CurrentUser> userProvider;
   private final GitHubConfig config;
+  private final VirtualDomainConfig virtualDomainConfig;
+  private final CanonicalWebUrls canonicalWebUrls;
+  private final AuthConfig authConfig;
 
   @Inject
   public VelocityViewServlet(
@@ -56,13 +64,19 @@
       Provider<PluginVelocityModel> modelProvider,
       ScopedProvider<GitHubLogin> loginProvider,
       Provider<CurrentUser> userProvider,
-      GitHubConfig config) {
+      GitHubConfig config,
+      VirtualDomainConfig virutalDomainConfig,
+      CanonicalWebUrls canonicalWebUrls,
+      AuthConfig authConfig) {
 
     this.velocityRuntime = velocityRuntime;
     this.modelProvider = modelProvider;
     this.loginProvider = loginProvider;
     this.userProvider = userProvider;
     this.config = config;
+    this.virtualDomainConfig = virutalDomainConfig;
+    this.canonicalWebUrls = canonicalWebUrls;
+    this.authConfig = authConfig;
   }
 
   @Override
@@ -71,6 +85,12 @@
     HttpServletRequest req = (HttpServletRequest) request;
     HttpServletResponse resp = (HttpServletResponse) response;
 
+    if (!(req.getRequestURI().equals(GITHUB_PLUGIN_OAUTH_SCOPE)
+        || userProvider.get().isIdentifiedUser())) {
+      resp.sendRedirect(authConfig.getLoginUrl());
+      return;
+    }
+
     String pathInfo =
         STATIC_PREFIX
             + MoreObjects.firstNonNull((String) req.getAttribute("destUrl"), req.getPathInfo());
@@ -96,6 +116,8 @@
     GitHubLogin gitHubLogin = loginProvider.get(request);
     model.put("myself", gitHubLogin.getMyself());
     model.put("config", config);
+    model.put("scopeSelectionUrl", canonicalWebUrls.getScopeSelectionUrl());
+    model.put("scopes", virtualDomainConfig.getScopes(request));
 
     CurrentUser user = userProvider.get();
     if (user.isIdentifiedUser()) {
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/wizard/AccountController.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/wizard/AccountController.java
index fc578af..0bb7e3e 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/wizard/AccountController.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/wizard/AccountController.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ServerInitiated;
-import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountsUpdate;
@@ -65,7 +64,6 @@
   private final AddSshKey restAddSshKey;
   private final GetSshKeys restGetSshKeys;
   private final AccountManager accountManager;
-  private final AccountCache accountCache;
   private final PutPreferred putPreferred;
   private final PutName putName;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
@@ -79,7 +77,6 @@
       final AddSshKey restAddSshKey,
       final GetSshKeys restGetSshKeys,
       final AccountManager accountManager,
-      final AccountCache accountCache,
       final PutPreferred putPreferred,
       final PutName putName,
       @ServerInitiated final Provider<AccountsUpdate> accountsUpdateProvider,
@@ -90,7 +87,6 @@
     this.restAddSshKey = restAddSshKey;
     this.restGetSshKeys = restGetSshKeys;
     this.accountManager = accountManager;
-    this.accountCache = accountCache;
     this.putPreferred = putPreferred;
     this.putName = putName;
     this.accountsUpdateProvider = accountsUpdateProvider;
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/wizard/PullRequestListController.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/wizard/PullRequestListController.java
index e47f161..b7fc801 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/wizard/PullRequestListController.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/wizard/PullRequestListController.java
@@ -101,7 +101,7 @@
           JsonArray prArray = new JsonArray();
           for (GHPullRequest pr : repoEntry.getValue()) {
             JsonObject prObj = new JsonObject();
-            prObj.add("id", new JsonPrimitive(new Integer(pr.getNumber())));
+            prObj.add("id", new JsonPrimitive(Integer.valueOf(pr.getNumber())));
             prObj.add("title", new JsonPrimitive(Strings.nullToEmpty(pr.getTitle())));
             prObj.add("body", new JsonPrimitive(Strings.nullToEmpty(pr.getBody())));
             prObj.add(
@@ -137,7 +137,7 @@
         if (githubRepo.isPresent()) {
           numPullRequests =
               collectPullRequestsFromGitHubRepository(
-                  numPullRequests, allPullRequests, gitRepo, ghRepoName, githubRepo);
+                  numPullRequests, allPullRequests, gitRepo, ghRepoName, githubRepo.get());
         }
       }
     }
@@ -149,13 +149,14 @@
       Map<String, List<GHPullRequest>> allPullRequests,
       Repository gitRepo,
       String ghRepoName,
-      Optional<GHRepository> githubRepo)
+      GHRepository githubRepo)
       throws IncorrectObjectTypeException, IOException {
     List<GHPullRequest> repoPullRequests = Lists.newArrayList();
 
     int count = numPullRequests;
     if (count < config.pullRequestListLimit) {
-      for (GHPullRequest ghPullRequest : githubRepo.get().listPullRequests(GHIssueState.OPEN)) {
+      for (GHPullRequest ghPullRequest :
+          githubRepo.queryPullRequests().state(GHIssueState.OPEN).list()) {
 
         if (isAnyCommitOfPullRequestToBeImported(gitRepo, ghPullRequest)) {
           repoPullRequests.add(ghPullRequest);
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/wizard/RepositoriesListController.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/wizard/RepositoriesListController.java
index 2cf6170..01a3d25 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/wizard/RepositoriesListController.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/wizard/RepositoriesListController.java
@@ -72,7 +72,7 @@
           repository.add("organisation", new JsonPrimitive(organisation));
           repository.add(
               "description", new JsonPrimitive(Strings.nullToEmpty(ghRepository.getDescription())));
-          repository.add("private", new JsonPrimitive(new Boolean(ghRepository.isPrivate())));
+          repository.add("private", new JsonPrimitive(Boolean.valueOf(ghRepository.isPrivate())));
           jsonRepos.add(repository);
           numRepos++;
         }
diff --git a/github-plugin/src/main/resources/Documentation/config.md b/github-plugin/src/main/resources/Documentation/config.md
index c9eebc5..4adcd41 100644
--- a/github-plugin/src/main/resources/Documentation/config.md
+++ b/github-plugin/src/main/resources/Documentation/config.md
@@ -77,6 +77,10 @@
     Default is empty read-only access to public 
     information (includes public user profile info, public repository info, and gists).
 
+github.<domain>.scopes
+:   Use only in conjunction with the `virtualhost` plugin to provide different GitHub scopes
+    selections for each virtual domain. It works the same way as `github.scopes`.
+
 github.httpConnectionTimeout
 :   Maximum time to wait for GitHub API to answer to a new HTTP connection attempt.
     Values should use common common unit unit suffixes to express their setting:
diff --git a/github-plugin/src/main/resources/static/repositories.html b/github-plugin/src/main/resources/static/repositories.html
index 9db7e3f..5a3df08 100644
--- a/github-plugin/src/main/resources/static/repositories.html
+++ b/github-plugin/src/main/resources/static/repositories.html
@@ -53,7 +53,7 @@
               </select>
               <input type="text" id="filter" class="filter" name="filter" placeholder="Filter by name" />
             </li>
-            <li class="info"><p>Not seeing your organizations or repositories? <a href="$config.getScopeSelectionUrl(null)">Login with a different GitHub Scope</a> and try again.</p></li>
+            <li class="info"><p>Not seeing your organizations or repositories? <a href="$scopeSelectionUrl">Login with a different GitHub Scope</a> and try again.</p></li>
           </ul>
           <div class="loading">
             <p>Loading list of GitHub repositories ...</p>
diff --git a/github-plugin/src/main/resources/static/scope.html b/github-plugin/src/main/resources/static/scope.html
index 113b5b5..ebc2580 100644
--- a/github-plugin/src/main/resources/static/scope.html
+++ b/github-plugin/src/main/resources/static/scope.html
@@ -6,6 +6,14 @@
     #include ("static/styles.html")
     #include ("static/scripts.html")
   </head>
+  <script type="text/javascript">
+    function submitOAuth() {
+      const submitButton =$("button[type='submit']");
+      submitButton.attr("disabled", true);
+      submitButton.find("span").text("Waiting for GitHub API response...");
+      document.forms[0].submit();
+    }
+  </script>
   <body>
     <!-- div.header start -->
     <div class="header">
@@ -19,7 +27,7 @@
                 <button type="button" onclick="window.location='/'" id="cancel">
                   <span class="button"><span>Cancel</span></span>
                 </button>
-                <button type="submit" onclick="document.forms[0].submit()">
+                <button type="submit" onclick="submitOAuth()">
                   <span class="button green"><span>Login &gt;</span></span>
                 </button>
               </div>
@@ -36,7 +44,7 @@
         <form class="signupform" method="get" action="/login">
           <h5>Which level of GitHub access do you need?</h5>
           <ul class="scopes">
-                #foreach ( $scope in $config.sortedScopesKeys )
+                #foreach ( $scope in $scopes.keySet() )
                     <li>
                         #set ( $scopeName = $scope.name.substring(6) )
                         #set ( $scopeDescription = $scope.description )
@@ -52,7 +60,7 @@
                         #end
                         <p class="scopeDescription">$scopeDescription</p>
                         <p class="scopePermissions">Allow to:
-                            #set ( $scopeItems = $config.scopes.get($scope) )
+                            #set ( $scopeItems = $scopes.get($scope) )
                             #foreach ( $scopeItem in $scopeItems )
                                 $scopeItem.description
                                 #if ( $foreach.count < $scopeItems.size())
diff --git a/github-plugin/src/main/ts/gr-github-oauth-progress.ts b/github-plugin/src/main/ts/gr-github-oauth-progress.ts
new file mode 100644
index 0000000..3b53d47
--- /dev/null
+++ b/github-plugin/src/main/ts/gr-github-oauth-progress.ts
@@ -0,0 +1,72 @@
+import { PluginApi } from '@gerritcodereview/typescript-api/plugin';
+import { AuthInfo } from '@gerritcodereview/typescript-api/rest-api';
+import { CSSResult, LitElement, css, html } from "lit";
+import { customElement, property, query, state } from 'lit/decorators.js';
+
+@customElement('gr-github-oauth-progress')
+export class GrGitHubOAuthProgress extends LitElement {
+    @query('#gitHubOAuthProgress')
+    gitHubOAuthProgress?: HTMLDialogElement;
+
+    @property() plugin!: PluginApi;
+
+    @state() authInfo?: AuthInfo
+
+    @state() loggedIn?: boolean
+
+    override connectedCallback() {
+        super.connectedCallback();
+        const restApi = this.plugin.restApi();
+        if (!this.authInfo) {
+            restApi.getConfig().then(config => this.authInfo = config?.auth);
+        }
+        restApi.getLoggedIn().then(loggedIn => this.loggedIn = loggedIn);
+    }
+
+    static override get styles() {
+        return [
+            window.Gerrit.styles.spinner as CSSResult,
+            window.Gerrit.styles.font as CSSResult,
+            window.Gerrit.styles.modal as CSSResult,
+            css`
+            .loginButton {
+                --gr-button-text-color: var(--header-text-color);
+                color: var(--header-text-color);
+                padding: var(--spacing-m) var(--spacing-l);
+            }
+            .loadingContainer {
+                display: flex;
+                gap: var(--spacing-s);
+                align-items: baseline;
+                padding: var(--spacing-xxl);
+            }
+            .loadingSpin {
+                vertical-align: top;
+                position: relative;
+                top: 3px;
+            }
+          `];
+    }
+
+    override render() {
+        if (!this.authInfo || this.loggedIn !== false) {
+            return
+        }
+
+        return html`
+            <a class="loginButton" href=${this.authInfo.login_url} @click=${this.showModal}>
+                ${this.authInfo.login_text}
+            </a>
+            <dialog id="gitHubOAuthProgress">
+                <div class="loadingContainer">
+                    <span class="loadingSpin"></span>
+                    <span class="loadingText">Waiting for GitHub API response ...</span>
+                </div>
+           </dialog>
+        `
+    }
+
+    private showModal() {
+        setTimeout(() => this.gitHubOAuthProgress?.showModal(), 550);
+    }
+}
diff --git a/github-plugin/src/main/ts/main.ts b/github-plugin/src/main/ts/main.ts
new file mode 100644
index 0000000..2a465ca
--- /dev/null
+++ b/github-plugin/src/main/ts/main.ts
@@ -0,0 +1,9 @@
+import '@gerritcodereview/typescript-api/gerrit';
+import './gr-github-oauth-progress';
+
+window.Gerrit.install(plugin => {
+    plugin.registerCustomComponent(
+        'auth-link',
+        'gr-github-oauth-progress',
+        { replace: true });
+});
diff --git a/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/FakeHttpSession.java b/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/FakeHttpSession.java
new file mode 100644
index 0000000..6a820e7
--- /dev/null
+++ b/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/FakeHttpSession.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2023 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.github;
+
+import java.util.Enumeration;
+import java.util.HashMap;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionContext;
+
+public class FakeHttpSession implements HttpSession {
+  private final HashMap<String, Object> attributes;
+
+  public FakeHttpSession() {
+    this.attributes = new HashMap<>();
+  }
+
+  @Override
+  public long getCreationTime() {
+    return 0;
+  }
+
+  @Override
+  public String getId() {
+    return null;
+  }
+
+  @Override
+  public long getLastAccessedTime() {
+    return 0;
+  }
+
+  @Override
+  public ServletContext getServletContext() {
+    return null;
+  }
+
+  @Override
+  public void setMaxInactiveInterval(int i) {}
+
+  @Override
+  public int getMaxInactiveInterval() {
+    return 0;
+  }
+
+  @Override
+  public HttpSessionContext getSessionContext() {
+    return null;
+  }
+
+  @Override
+  public Object getAttribute(String s) {
+    return attributes.get(s);
+  }
+
+  @Override
+  public Object getValue(String s) {
+    return getAttribute(s);
+  }
+
+  @Override
+  public Enumeration<String> getAttributeNames() {
+    return java.util.Collections.enumeration(attributes.keySet());
+  }
+
+  @Override
+  public String[] getValueNames() {
+    return attributes.keySet().toArray(new String[0]);
+  }
+
+  @Override
+  public void setAttribute(String s, Object o) {
+    attributes.put(s, o);
+  }
+
+  @Override
+  public void putValue(String s, Object o) {
+    setAttribute(s, o);
+  }
+
+  @Override
+  public void removeAttribute(String s) {
+    attributes.remove(s);
+  }
+
+  @Override
+  public void removeValue(String s) {
+    removeAttribute(s);
+  }
+
+  @Override
+  public void invalidate() {
+    attributes.clear();
+  }
+
+  @Override
+  public boolean isNew() {
+    return false;
+  }
+}
diff --git a/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/FanoutReplicationConfigTest.java b/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/FanoutReplicationConfigTest.java
new file mode 100644
index 0000000..3613c41
--- /dev/null
+++ b/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/FanoutReplicationConfigTest.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2023 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.github;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.server.config.SitePaths;
+import com.googlesource.gerrit.plugins.github.git.FanoutReplicationConfig;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.apache.commons.io.FileUtils;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class FanoutReplicationConfigTest {
+
+  private static final String CUSTOM_KEY = "mykey";
+  private static final String CUSTOM_VALUE = "myvalue";
+  private static final String REMOTE_ENDPOINT = "my-remote-endpoint";
+  private static final String TEST_REMOTE_URL = "http://github.com/myurl";
+  private static final String TEST_PROJECT_NAME = "myprojectname";
+  private Path tempDir;
+  private SitePaths sitePaths;
+
+  @Before
+  public void setup() throws Exception {
+    tempDir = Files.createTempDirectory(getClass().getSimpleName());
+    sitePaths = new SitePaths(tempDir);
+    Files.createDirectories(sitePaths.etc_dir);
+  }
+
+  @After
+  public void teardown() throws Exception {
+    FileUtils.deleteDirectory(tempDir.toFile());
+  }
+
+  @Test
+  public void shoudKeepAdHocSettingsInFanoutReplicationConfig() throws Exception {
+    FileBasedConfig currConfig = getReplicationConfig();
+    currConfig.setString("remote", null, CUSTOM_KEY, CUSTOM_VALUE);
+    currConfig.save();
+
+    String url = "http://github.com/myurl";
+    FanoutReplicationConfig fanoutReplicationConfig = new FanoutReplicationConfig(sitePaths);
+    fanoutReplicationConfig.addReplicationRemote(REMOTE_ENDPOINT, url, "myproject");
+
+    currConfig.load();
+    assertThat(currConfig.getString("remote", null, CUSTOM_KEY)).isEqualTo(CUSTOM_VALUE);
+  }
+
+  @Test
+  public void shoudKeepCustomUrlInFanoutReplicationConfig() throws Exception {
+    FileBasedConfig currConfig = getReplicationConfig();
+    String customUrl = "http://my-custom-url";
+    currConfig.setString("remote", null, "url", customUrl);
+    currConfig.save();
+
+    new FanoutReplicationConfig(sitePaths)
+        .addReplicationRemote(REMOTE_ENDPOINT, TEST_REMOTE_URL, TEST_PROJECT_NAME);
+
+    currConfig.load();
+    assertThat(currConfig.getString("remote", null, "url")).isEqualTo(customUrl);
+  }
+
+  @Test
+  public void shoudKeepCustomPushRefSpecInFanoutReplicationConfig() throws Exception {
+    FileBasedConfig currConfig = getReplicationConfig();
+    String customPushRefSpec = "+refs/heads/myheads/*:refs/heads/myheads/*";
+    currConfig.setString("remote", null, "push", customPushRefSpec);
+    currConfig.save();
+
+    new FanoutReplicationConfig(sitePaths)
+        .addReplicationRemote(REMOTE_ENDPOINT, TEST_REMOTE_URL, TEST_PROJECT_NAME);
+
+    currConfig.load();
+    assertThat(currConfig.getString("remote", null, "push")).isEqualTo(customPushRefSpec);
+  }
+
+  private FileBasedConfig getReplicationConfig() {
+    return new FileBasedConfig(
+        sitePaths.etc_dir.resolve("replication").resolve(REMOTE_ENDPOINT + ".config").toFile(),
+        FS.DETECTED);
+  }
+}
diff --git a/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/GitHubConfigTest.java b/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/GitHubConfigTest.java
index 56f7b90..b7dfc60 100644
--- a/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/GitHubConfigTest.java
+++ b/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/GitHubConfigTest.java
@@ -117,6 +117,6 @@
             + "clientId = myclientid\n"
             + "clientSecret = mysecret\n"
             + configText);
-    return new GitHubConfig(gerritConfig, site, ALL_PROJECTS_NAME_PROVIDER, null);
+    return new GitHubConfig(gerritConfig, site, ALL_PROJECTS_NAME_PROVIDER);
   }
 }
diff --git a/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/GitHubGroupCacheRefreshFilterTest.java b/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/GitHubGroupCacheRefreshFilterTest.java
new file mode 100644
index 0000000..5c1707f
--- /dev/null
+++ b/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/GitHubGroupCacheRefreshFilterTest.java
@@ -0,0 +1,125 @@
+// Copyright (C) 2023 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.github;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
+import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
+import com.googlesource.gerrit.plugins.github.filters.GitHubGroupCacheRefreshFilter;
+import com.googlesource.gerrit.plugins.github.group.GitHubGroupsCache;
+import com.googlesource.gerrit.plugins.github.groups.OrganizationStructure;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GitHubGroupCacheRefreshFilterTest {
+  private static final FilterChain NOOP_FILTER_CHAIN_TEST = (req, res) -> {};
+  private static final String GITHUB_USERNAME_TEST = "somegithubuser";
+  private static final OrganizationStructure GITHUB_USER_ORGANIZATION = new OrganizationStructure();
+  private static final String TEST_SERVER = "test-server";
+  private static final int TEST_PORT = 80;
+
+  private LoadingCache<String, OrganizationStructure> groupsByUsernameCache;
+  private GitHubGroupCacheRefreshFilter filter;
+  private FakeGroupCacheLoader groupsCacheLoader;
+  private int initialLoadCount;
+
+  private static class FakeGroupCacheLoader extends CacheLoader<String, OrganizationStructure> {
+    private final String username;
+    private final OrganizationStructure organizationStructure;
+    private int loadCount;
+
+    FakeGroupCacheLoader(String username, OrganizationStructure organizationStructure) {
+      this.username = username;
+      this.organizationStructure = organizationStructure;
+    }
+
+    @Override
+    public OrganizationStructure load(String u) throws Exception {
+      if (u.equals(username)) {
+        loadCount++;
+        return organizationStructure;
+      } else {
+        return null;
+      }
+    }
+
+    public int getLoadCount() {
+      return loadCount;
+    }
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    groupsCacheLoader = new FakeGroupCacheLoader(GITHUB_USERNAME_TEST, GITHUB_USER_ORGANIZATION);
+    groupsByUsernameCache = CacheBuilder.newBuilder().build(groupsCacheLoader);
+    filter =
+        new GitHubGroupCacheRefreshFilter(
+            new GitHubGroupsCache(groupsByUsernameCache, () -> GITHUB_USERNAME_TEST));
+    // Trigger the initial load of the groups cache
+    assertThat(groupsByUsernameCache.get(GITHUB_USERNAME_TEST)).isEqualTo(GITHUB_USER_ORGANIZATION);
+    initialLoadCount = groupsCacheLoader.getLoadCount();
+  }
+
+  @Test
+  public void shouldReloadGroupsUponSuccessfulLogin() throws Exception {
+    FakeHttpServletRequest finalLoginRequest = newFinalLoginRequest();
+    filter.doFilter(finalLoginRequest, newFinalLoginRedirectWithCookie(), NOOP_FILTER_CHAIN_TEST);
+    filter.doFilter(
+        newHomepageRequest(finalLoginRequest.getSession()),
+        new FakeHttpServletResponse(),
+        NOOP_FILTER_CHAIN_TEST);
+
+    assertThat(groupsByUsernameCache.get(GITHUB_USERNAME_TEST)).isEqualTo(GITHUB_USER_ORGANIZATION);
+    assertThat(groupsCacheLoader.getLoadCount()).isEqualTo(initialLoadCount + 1);
+  }
+
+  @Test
+  public void shouldNotReloadGroupsOnRegularRequests() throws Exception {
+    FakeHttpServletRequest regularRequest = new FakeHttpServletRequest();
+    filter.doFilter(regularRequest, new FakeHttpServletResponse(), NOOP_FILTER_CHAIN_TEST);
+    filter.doFilter(
+        newHomepageRequest(null), new FakeHttpServletResponse(), NOOP_FILTER_CHAIN_TEST);
+
+    assertThat(groupsByUsernameCache.get(GITHUB_USERNAME_TEST)).isEqualTo(GITHUB_USER_ORGANIZATION);
+    assertThat(groupsCacheLoader.getLoadCount()).isEqualTo(initialLoadCount);
+  }
+
+  private ServletRequest newHomepageRequest(HttpSession session) {
+    return new FakeHttpServletRequest(TEST_SERVER, TEST_PORT, "", "/", null, session);
+  }
+
+  private static HttpServletResponse newFinalLoginRedirectWithCookie() {
+    HttpServletResponse res = new FakeHttpServletResponse();
+    res.setHeader("Set-Cookie", "GerritAccount=foo");
+    return res;
+  }
+
+  private static FakeHttpServletRequest newFinalLoginRequest() {
+    FakeHttpServletRequest req =
+        new FakeHttpServletRequest(
+            TEST_SERVER, TEST_PORT, "", "", () -> new FakeHttpSession(), null);
+    req.setQueryString("final=true");
+    req.setPathInfo("/login");
+    return req;
+  }
+}
diff --git a/github-plugin/tsconfig-plugins-base.json b/github-plugin/tsconfig-plugins-base.json
new file mode 100644
index 0000000..a19ebbf
--- /dev/null
+++ b/github-plugin/tsconfig-plugins-base.json
@@ -0,0 +1,50 @@
+/* TODO: this file should be included in @gerritcodereview/typescript-api */
+{
+  "compilerOptions": {
+    /* Basic Options */
+    "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
+    "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
+    "inlineSourceMap": true, /* Generates corresponding '.map' file. */
+    "rootDir": ".", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+    "removeComments": false, /* Emit comments to output */
+
+    /* Strict Type-Checking Options */
+    "strict": true, /* Enable all strict type-checking options. */
+    "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
+    "strictNullChecks": true, /* Enable strict null checks. */
+    "strictFunctionTypes": true, /* Enable strict checking of function types. */
+    "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+    "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
+    "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
+
+    /* Additional Checks */
+    "noUnusedLocals": true, /* Report errors on unused locals. */
+    "noUnusedParameters": true, /* Report errors on unused parameters. */
+    "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+    "noImplicitOverride": true,
+    "noFallthroughCasesInSwitch": true,/* Report errors for fallthrough cases in switch statement. */
+
+    "skipLibCheck": true, /* Do not check node_modules */
+
+    /* Module Resolution Options */
+    "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+    "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+    "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
+
+    /* Advanced Options */
+    "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
+    "incremental": true,
+    "experimentalDecorators": true,
+
+    "allowUmdGlobalAccess": true,
+
+    "typeRoots": [
+      /* typeRoots for Bazel */
+      "../external/ui_dev_npm/node_modules/@types",
+      "../external/plugins_npm/node_modules/@types",
+      /* typeRoots for IDE */
+      "../polygerrit-ui/node_modules/@types",
+      "../plugins/node_modules/@types"
+    ]
+  },
+}
diff --git a/github-plugin/tsconfig.json b/github-plugin/tsconfig.json
new file mode 100644
index 0000000..f08de5a
--- /dev/null
+++ b/github-plugin/tsconfig.json
@@ -0,0 +1,10 @@
+{
+  /* TODO: should be change to ./node_modules/@gerritcodereview/typescript-api/tsconfig-plugins-base.json' when NPM paclage is fixed */
+  "extends": "./tsconfig-plugins-base.json",
+  "compilerOptions": {
+    "rootDir": ".",
+    "experimentalDecorators": true,
+    "skipLibCheck": true,
+    "outDir": "./target/web"
+  },
+}
\ No newline at end of file
diff --git a/github-plugin/yarn.lock b/github-plugin/yarn.lock
new file mode 100644
index 0000000..51435d5
--- /dev/null
+++ b/github-plugin/yarn.lock
@@ -0,0 +1,316 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@gerritcodereview/typescript-api@^3.8.0":
+  version "3.8.0"
+  resolved "https://registry.yarnpkg.com/@gerritcodereview/typescript-api/-/typescript-api-3.8.0.tgz#2e418b814d7451c40365b2dc4f88e9965ece0769"
+  integrity sha512-wUkIWUx99Rj1vxRYQISxyzN0nplqu7t5sRDyJ8R3yNNkvALQAMC6Whj63qzCsZsymVFzC5up3y+ZVxaeh7b+xA==
+
+"@jridgewell/gen-mapping@^0.3.0":
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
+  integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==
+  dependencies:
+    "@jridgewell/set-array" "^1.0.1"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/resolve-uri@^3.1.0":
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
+  integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
+
+"@jridgewell/set-array@^1.0.1":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
+  integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+
+"@jridgewell/source-map@^0.3.3":
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91"
+  integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==
+  dependencies:
+    "@jridgewell/gen-mapping" "^0.3.0"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
+  version "1.4.15"
+  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
+  integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+
+"@jridgewell/trace-mapping@^0.3.9":
+  version "0.3.19"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811"
+  integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==
+  dependencies:
+    "@jridgewell/resolve-uri" "^3.1.0"
+    "@jridgewell/sourcemap-codec" "^1.4.14"
+
+"@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz#64df34e2f12e68e78ac57e571d25ec07fa460ca9"
+  integrity sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ==
+
+"@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.6.0":
+  version "1.6.3"
+  resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.6.3.tgz#25b4eece2592132845d303e091bad9b04cdcfe03"
+  integrity sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==
+  dependencies:
+    "@lit-labs/ssr-dom-shim" "^1.0.0"
+
+"@lit/ts-transformers@^1.1.3":
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/@lit/ts-transformers/-/ts-transformers-1.1.3.tgz#0d6c99c9a619dc762f896bd403546a7e396942be"
+  integrity sha512-I3Pp2J9SS09h3SiMxOQ87vVPZA74qZfYR1rD5by8F6VXYYwmN8DEe52tpi/u4Na2wE/XmkFgAg/vsVWz0fqvuw==
+  dependencies:
+    ts-clone-node "^1.0.0"
+    typescript "~4.7.4"
+
+"@polymer/polymer@^3.5.1":
+  version "3.5.1"
+  resolved "https://registry.yarnpkg.com/@polymer/polymer/-/polymer-3.5.1.tgz#4b5234e43b8876441022bcb91313ab3c4a29f0c8"
+  integrity sha512-JlAHuy+1qIC6hL1ojEUfIVD58fzTpJAoCxFwV5yr0mYTXV1H8bz5zy0+rC963Cgr9iNXQ4T9ncSjC2fkF9BQfw==
+  dependencies:
+    "@webcomponents/shadycss" "^1.9.1"
+
+"@rollup/plugin-node-resolve@^15.2.1":
+  version "15.2.1"
+  resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.1.tgz#a15b14fb7969229e26a30feff2816d39eff503f0"
+  integrity sha512-nsbUg588+GDSu8/NS8T4UAshO6xeaOfINNuXeVHcKV02LJtoRaM1SiOacClw4kws1SFiNhdLGxlbMY9ga/zs/w==
+  dependencies:
+    "@rollup/pluginutils" "^5.0.1"
+    "@types/resolve" "1.20.2"
+    deepmerge "^4.2.2"
+    is-builtin-module "^3.2.1"
+    is-module "^1.0.0"
+    resolve "^1.22.1"
+
+"@rollup/plugin-terser@^0.4.3":
+  version "0.4.3"
+  resolved "https://registry.yarnpkg.com/@rollup/plugin-terser/-/plugin-terser-0.4.3.tgz#c2bde2fe3a85e45fa68a454d48f4e73e57f98b30"
+  integrity sha512-EF0oejTMtkyhrkwCdg0HJ0IpkcaVg1MMSf2olHb2Jp+1mnLM04OhjpJWGma4HobiDTF0WCyViWuvadyE9ch2XA==
+  dependencies:
+    serialize-javascript "^6.0.1"
+    smob "^1.0.0"
+    terser "^5.17.4"
+
+"@rollup/pluginutils@^5.0.1":
+  version "5.0.4"
+  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.4.tgz#74f808f9053d33bafec0cc98e7b835c9667d32ba"
+  integrity sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==
+  dependencies:
+    "@types/estree" "^1.0.0"
+    estree-walker "^2.0.2"
+    picomatch "^2.3.1"
+
+"@types/estree@^1.0.0":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.2.tgz#ff02bc3dc8317cd668dfec247b750ba1f1d62453"
+  integrity sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==
+
+"@types/resolve@1.20.2":
+  version "1.20.2"
+  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975"
+  integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==
+
+"@types/trusted-types@^2.0.2":
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.4.tgz#2b38784cd16957d3782e8e2b31c03bc1d13b4d65"
+  integrity sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==
+
+"@webcomponents/shadycss@^1.9.1":
+  version "1.11.2"
+  resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.11.2.tgz#7539b0ad29598aa2eafee8b341059e20ac9e1006"
+  integrity sha512-vRq+GniJAYSBmTRnhCYPAPq6THYqovJ/gzGThWbgEZUQaBccndGTi1hdiUP15HzEco0I6t4RCtXyX0rsSmwgPw==
+
+acorn@^8.8.2:
+  version "8.10.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
+  integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
+
+buffer-from@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+builtin-modules@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
+  integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==
+
+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==
+
+compatfactory@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/compatfactory/-/compatfactory-1.0.1.tgz#a5940f1d734b86c02bb818a67a412d4c306ccaf4"
+  integrity sha512-hR9u0HSZTKDNNchPtMHg6myeNx0XO+av7UZIJPsi4rPALJBHi/W5Mbwi19hC/xm6y3JkYpxVYjTqnSGsU5X/iw==
+  dependencies:
+    helpertypes "^0.0.18"
+
+deepmerge@^4.2.2:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
+  integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
+
+estree-walker@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
+  integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
+
+fsevents@~2.3.2:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+  integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+has@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6"
+  integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==
+
+helpertypes@^0.0.18:
+  version "0.0.18"
+  resolved "https://registry.yarnpkg.com/helpertypes/-/helpertypes-0.0.18.tgz#fd2bf5d3351cc7d80f7876732361d3adba63e5b4"
+  integrity sha512-XRhfbSEmR+poXUC5/8AbmYNJb2riOT6qPzjGJZr0S9YedHiaY+/tzPYzWMUclYMEdCYo/1l8PDYrQFCj02v97w==
+
+is-builtin-module@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169"
+  integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==
+  dependencies:
+    builtin-modules "^3.3.0"
+
+is-core-module@^2.13.0:
+  version "2.13.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db"
+  integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==
+  dependencies:
+    has "^1.0.3"
+
+is-module@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
+  integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==
+
+lit-element@^3.3.0:
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.3.3.tgz#10bc19702b96ef5416cf7a70177255bfb17b3209"
+  integrity sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==
+  dependencies:
+    "@lit-labs/ssr-dom-shim" "^1.1.0"
+    "@lit/reactive-element" "^1.3.0"
+    lit-html "^2.8.0"
+
+lit-html@^2.8.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.8.0.tgz#96456a4bb4ee717b9a7d2f94562a16509d39bffa"
+  integrity sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==
+  dependencies:
+    "@types/trusted-types" "^2.0.2"
+
+lit@^2.8.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/lit/-/lit-2.8.0.tgz#4d838ae03059bf9cafa06e5c61d8acc0081e974e"
+  integrity sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==
+  dependencies:
+    "@lit/reactive-element" "^1.6.0"
+    lit-element "^3.3.0"
+    lit-html "^2.8.0"
+
+path-parse@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+picomatch@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+randombytes@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+  integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+  dependencies:
+    safe-buffer "^5.1.0"
+
+resolve@^1.22.1:
+  version "1.22.6"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362"
+  integrity sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==
+  dependencies:
+    is-core-module "^2.13.0"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
+
+rollup@^3.29.4:
+  version "3.29.4"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981"
+  integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+safe-buffer@^5.1.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+serialize-javascript@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c"
+  integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==
+  dependencies:
+    randombytes "^2.1.0"
+
+smob@^1.0.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/smob/-/smob-1.4.1.tgz#66270e7df6a7527664816c5b577a23f17ba6f5b5"
+  integrity sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==
+
+source-map-support@~0.5.20:
+  version "0.5.21"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
+  integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
+  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==
+
+supports-preserve-symlinks-flag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+terser@^5.17.4:
+  version "5.21.0"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.21.0.tgz#d2b27e92b5e56650bc83b6defa00a110f0b124b2"
+  integrity sha512-WtnFKrxu9kaoXuiZFSGrcAvvBqAdmKx0SFNmVNYdJamMu9yyN3I/QF0FbH4QcqJQ+y1CJnzxGIKH0cSj+FGYRw==
+  dependencies:
+    "@jridgewell/source-map" "^0.3.3"
+    acorn "^8.8.2"
+    commander "^2.20.0"
+    source-map-support "~0.5.20"
+
+ts-clone-node@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/ts-clone-node/-/ts-clone-node-1.0.0.tgz#aaffa5478cf303471cec9c3c8169e117a0f87614"
+  integrity sha512-/cDYbr2HAXxFNeTT41c/xs/2bhLJjqnYheHsmA3AoHSt+n4JA4t0FL9Lk5O8kWnJ6jeB3kPcUoXIFtwERNzv6Q==
+  dependencies:
+    compatfactory "^1.0.1"
+
+typescript@^4.9.5:
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
+  integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
+
+typescript@~4.7.4:
+  version "4.7.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
+  integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
diff --git a/pom.xml b/pom.xml
index 5d6b07e..d0f54d8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,12 +18,12 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.googlesource.gerrit.plugins.github</groupId>
   <artifactId>github-parent</artifactId>
-  <version>3.8.0</version>
+  <version>3.9.0-rc5</version>
   <name>Gerrit Code Review - GitHub integration</name>
   <url>http://www.gerritforge.com</url>
   <packaging>pom</packaging>
   <properties>
-    <javaVersion>11</javaVersion>
+    <javaVersion>17</javaVersion>
   </properties>
   <licenses>
     <license>
@@ -255,7 +255,7 @@
       <plugin>
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>findbugs-maven-plugin</artifactId>
-        <version>3.0.0</version>
+        <version>3.0.5</version>
       </plugin>
     </plugins>
   </reporting>
@@ -292,13 +292,13 @@
     <dependency>
         <groupId>com.ryanharter.auto.value</groupId>
         <artifactId>auto-value-gson</artifactId>
-        <version>1.3.0</version>
+        <version>1.3.1</version>
         <scope>provided</scope>
     </dependency>
     <dependency>
         <groupId>org.projectlombok</groupId>
         <artifactId>lombok</artifactId>
-        <version>1.18.22</version>
+        <version>1.18.30</version>
         <scope>provided</scope>
     </dependency>
   </dependencies>
@@ -313,7 +313,7 @@
         <dependency>
             <groupId>com.google.truth</groupId>
             <artifactId>truth</artifactId>
-            <version>1.1.4</version>
+            <version>1.1.5</version>
             <scope>test</scope>
         </dependency>
     </dependencies>