Merge branch 'stable-3.4'

* stable-3.4: (24 commits)
  ParsingQueuePersistenceIT: fix google-common imports
  EiffelEventParser: fix javadoc
  Persist failed tasks
  ParsingQueue: Save failed parsing-tasks
  Keep queued tasks to persist in a Set and not a List
  Correct spelling persistance -> persistence
  ParsingQueue: Move scheduling logic into worker
  Fix EiffelEventParserIt#artcQueued
  Refactor EiffelEventParser and implementations
  EiffelConfig: fix invalid log format
  EventKey: add missing cases to switch/case
  Fix event-name in routing key
  Persist parsing-queue on plugin stop
  EiffelEventParsingQueue: Google-java-format
  Refactor EiffelEventParserIT
  Make EiffelEventParsingQueue#scheduleArtcCreation private
  Extract private method to schedule SCS from submit-event
  Rename EiffelEventParser and EiffelEventParserIf
  Extract interface from EiffelEventParser
  Extract the task description from EventParsingWorker
  ...

Change-Id: I8d6f26aef920b9b7ccd967a5f7f164a02b9c6650
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/cache/EiffelEventIdCacheImpl.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/cache/EiffelEventIdCacheImpl.java
index 5377521..fa7d622 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/cache/EiffelEventIdCacheImpl.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/cache/EiffelEventIdCacheImpl.java
@@ -19,7 +19,6 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
-import com.google.common.cache.Weigher;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -53,7 +52,6 @@
         persist(CACHE_NAME, EventKey.class, new TypeLiteral<Optional<UUID>>() {})
             .maximumWeight(maxNbrOfEntries)
             .loader(EventLoader.class)
-            .weigher(CountWeigher.class)
             .keySerializer(new EventKeySerializer())
             .valueSerializer(new UUIDSerializer());
         bind(EiffelEventIdCacheImpl.class).asEagerSingleton();
@@ -155,12 +153,4 @@
       return es.getEventId(key);
     }
   }
-
-  static class CountWeigher implements Weigher<EventKey, Optional<UUID>> {
-
-    @Override
-    public int weigh(EventKey key, Optional<UUID> value) {
-      return 1;
-    }
-  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/config/EventListenersConfig.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/config/EventListenersConfig.java
index 32f51c5..cc89f39 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/config/EventListenersConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/config/EventListenersConfig.java
@@ -16,15 +16,11 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.AccessSection;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.inject.Inject;
-import java.util.Arrays;
-import java.util.function.Function;
-import java.util.regex.Pattern;
 import org.eclipse.jgit.lib.Config;
 
 public class EventListenersConfig {
@@ -32,31 +28,10 @@
     private static final FluentLogger logger = FluentLogger.forEnclosingClass();
     static final String EVENT_LISTENERS = "EventListeners";
     static final String ENABLED = "enabled";
-    static final String BLOCKED_REFS = "blockedRef";
-    static final String BLOCKED_PROJECTS = "blockedProject";
-
-    @VisibleForTesting
-    static boolean filterMatches(String filter, String repoName) {
-      if (filter.startsWith(AccessSection.REGEX_PREFIX)) {
-        return Pattern.matches(filter, repoName);
-      }
-      if (filter.endsWith("*")) {
-        return repoName.startsWith(filter.substring(0, filter.length() - 1));
-      }
-      return repoName.equals(filter);
-    }
-
-    @VisibleForTesting
-    public static Function<String, Boolean> toCombinedMatcher(String... filters) {
-      return repoName -> Arrays.stream(filters).anyMatch(f -> filterMatches(f, repoName));
-    }
 
     @VisibleForTesting
     static EventListenersConfig fromConfig(Config cfg) {
-      return new EventListenersConfig(
-          cfg.getBoolean(EVENT_LISTENERS, ENABLED, true),
-          toCombinedMatcher(cfg.getStringList(EVENT_LISTENERS, null, BLOCKED_PROJECTS)),
-          toCombinedMatcher(cfg.getStringList(EVENT_LISTENERS, null, BLOCKED_REFS)));
+      return new EventListenersConfig(cfg.getBoolean(EVENT_LISTENERS, ENABLED, true));
     }
 
     private final PluginConfigFactory configFactory;
@@ -81,34 +56,19 @@
       } catch (NoSuchProjectException e) {
         logger.atSevere().withCause(e).log(
             "Unable to read project.config, using default EventListenersConfig");
-        return new EventListenersConfig(true, toCombinedMatcher(""), toCombinedMatcher(""));
+        return new EventListenersConfig(true);
       }
     }
   }
 
   private final boolean enabled;
-  private final Function<String, Boolean> blockedRefMatcher;
-  private final Function<String, Boolean> blockedProjectMatcher;
 
   @VisibleForTesting
-  public EventListenersConfig(
-      boolean enabled,
-      Function<String, Boolean> blockedProjectMatcher,
-      Function<String, Boolean> blockedRefMatcher) {
+  public EventListenersConfig(boolean enabled) {
     this.enabled = enabled;
-    this.blockedProjectMatcher = blockedProjectMatcher;
-    this.blockedRefMatcher = blockedRefMatcher;
   }
 
   public boolean isEnabled() {
     return this.enabled;
   }
-
-  public boolean refIsBlocked(String ref) {
-    return blockedRefMatcher.apply(ref);
-  }
-
-  public boolean projectIsBlocked(String repoName) {
-    return blockedProjectMatcher.apply(repoName);
-  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/config/EventsFilter.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/config/EventsFilter.java
new file mode 100644
index 0000000..38f3acc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/config/EventsFilter.java
@@ -0,0 +1,114 @@
+// Copyright (C) 2022 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.eventseiffel.config;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.AccessSection;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.inject.Inject;
+import java.util.Arrays;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+import org.eclipse.jgit.lib.Config;
+
+public class EventsFilter {
+
+  public static class Provider implements com.google.inject.Provider<EventsFilter> {
+    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+    static final String EVENTS_FILTER = "EventsFilter";
+    static final String BLOCKED_REFS = "blockedRef";
+    static final String BLOCKED_PROJECTS = "blockedProject";
+    static final String BLOCK_LIGHTWEIGHT_TAGS = "blockLightWeightTags";
+
+    @VisibleForTesting
+    static boolean filterMatches(String filter, String repoName) {
+      if (filter.startsWith(AccessSection.REGEX_PREFIX)) {
+        return Pattern.matches(filter, repoName);
+      }
+      if (filter.endsWith("*")) {
+        return repoName.startsWith(filter.substring(0, filter.length() - 1));
+      }
+      return repoName.equals(filter);
+    }
+
+    @VisibleForTesting
+    public static Function<String, Boolean> toCombinedMatcher(String... filters) {
+      return repoName -> Arrays.stream(filters).anyMatch(f -> filterMatches(f, repoName));
+    }
+
+    @VisibleForTesting
+    static EventsFilter fromConfig(Config cfg) {
+      return new EventsFilter(
+          toCombinedMatcher(cfg.getStringList(EVENTS_FILTER, null, BLOCKED_PROJECTS)),
+          toCombinedMatcher(cfg.getStringList(EVENTS_FILTER, null, BLOCKED_REFS)),
+          cfg.getBoolean(EVENTS_FILTER, BLOCK_LIGHTWEIGHT_TAGS, false));
+    }
+
+    private final PluginConfigFactory configFactory;
+    private final String pluginName;
+    private final AllProjectsName allProjects;
+
+    @Inject
+    public Provider(
+        PluginConfigFactory cfgFactory,
+        @PluginName String pluginName,
+        AllProjectsName allProjects) {
+      this.configFactory = cfgFactory;
+      this.pluginName = pluginName;
+      this.allProjects = allProjects;
+    }
+
+    @Override
+    public EventsFilter get() {
+      try {
+        Config cfg = configFactory.getProjectPluginConfig(allProjects, pluginName);
+        return fromConfig(cfg);
+      } catch (NoSuchProjectException e) {
+        logger.atSevere().withCause(e).log(
+            "Unable to read project.config, using default EventsFilter");
+        return new EventsFilter(toCombinedMatcher(""), toCombinedMatcher(""), false);
+      }
+    }
+  }
+
+  private final Function<String, Boolean> blockedRefMatcher;
+  private final Function<String, Boolean> blockedProjectMatcher;
+  private final boolean blockLightWeightTags;
+
+  @VisibleForTesting
+  public EventsFilter(
+      Function<String, Boolean> blockedProjectMatcher,
+      Function<String, Boolean> blockedRefMatcher,
+      boolean blockLightWeightTags) {
+    this.blockedProjectMatcher = blockedProjectMatcher;
+    this.blockedRefMatcher = blockedRefMatcher;
+    this.blockLightWeightTags = blockLightWeightTags;
+  }
+
+  public boolean refIsBlocked(String ref) {
+    return blockedRefMatcher.apply(ref);
+  }
+
+  public boolean projectIsBlocked(String repoName) {
+    return blockedProjectMatcher.apply(repoName);
+  }
+
+  public boolean blockLightWeightTags() {
+    return blockLightWeightTags;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/eiffel/api/EiffelGraphQlClient.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/eiffel/api/EiffelGraphQlClient.java
index c3c9201..ff8bf2b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/eiffel/api/EiffelGraphQlClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/eiffel/api/EiffelGraphQlClient.java
@@ -14,6 +14,11 @@
 
 package com.googlesource.gerrit.plugins.eventseiffel.eiffel.api;
 
+import com.github.rholder.retry.RetryException;
+import com.github.rholder.retry.Retryer;
+import com.github.rholder.retry.RetryerBuilder;
+import com.github.rholder.retry.StopStrategies;
+import com.github.rholder.retry.WaitStrategies;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
@@ -37,6 +42,8 @@
 import java.util.List;
 import java.util.Optional;
 import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 public class EiffelGraphQlClient implements EventStorage {
@@ -131,8 +138,14 @@
   private QueryResult query(String query) throws EventStorageException {
     HttpResponse<String> response;
     try {
-      response = post(query);
-    } catch (IOException | InterruptedException e) {
+      Retryer<HttpResponse<String>> retryer =
+          RetryerBuilder.<HttpResponse<String>>newBuilder()
+              .retryIfException()
+              .withWaitStrategy(WaitStrategies.fixedWait(10, TimeUnit.SECONDS))
+              .withStopStrategy(StopStrategies.stopAfterAttempt(2))
+              .build();
+      response = retryer.call(() -> post(query));
+    } catch (RetryException | ExecutionException e) {
       throw new EventStorageException(e, "Query \"%s\" failed.", query);
     }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/PatchsetCreatedListener.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/PatchsetCreatedListener.java
index 4d07ca8..048f701 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/PatchsetCreatedListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/PatchsetCreatedListener.java
@@ -18,30 +18,35 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.googlesource.gerrit.plugins.eventseiffel.config.EventListenersConfig;
+import com.googlesource.gerrit.plugins.eventseiffel.config.EventsFilter;
 import com.googlesource.gerrit.plugins.eventseiffel.parsing.EiffelEventParsingQueue;
 
 public class PatchsetCreatedListener implements RevisionCreatedListener {
   private final EiffelEventParsingQueue queue;
   private final Provider<EventListenersConfig> configProvider;
+  private final Provider<EventsFilter> eventsFilter;
 
   @Inject
   public PatchsetCreatedListener(
-      EiffelEventParsingQueue queue, Provider<EventListenersConfig> config) {
+      EiffelEventParsingQueue queue,
+      Provider<EventListenersConfig> configProvider,
+      Provider<EventsFilter> eventsFilter) {
     this.queue = queue;
-    this.configProvider = config;
+    this.configProvider = configProvider;
+    this.eventsFilter = eventsFilter;
   }
 
   @Override
   public void onRevisionCreated(RevisionCreatedListener.Event event) {
-    EventListenersConfig config = configProvider.get();
-    if (!config.isEnabled()) {
+    if (!configProvider.get().isEnabled()) {
       return;
     }
     /* 'branch' is not the exact reference in this case.
      * Filtering for startsWith("refs/") to filter out refs/meta/config.*/
     if (!event.getChange().branch.startsWith(RefNames.REFS)) {
-      if (config.refIsBlocked(RefNames.REFS_HEADS + event.getChange().branch)
-          || config.projectIsBlocked(event.getChange().project)) {
+      EventsFilter filter = eventsFilter.get();
+      if (filter.refIsBlocked(RefNames.REFS_HEADS + event.getChange().branch)
+          || filter.projectIsBlocked(event.getChange().project)) {
         return;
       }
       queue.scheduleSccCreation(event);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/RefUpdateListener.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/RefUpdateListener.java
index 98ca829..3a04f53 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/RefUpdateListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/RefUpdateListener.java
@@ -18,18 +18,23 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.googlesource.gerrit.plugins.eventseiffel.config.EventListenersConfig;
+import com.googlesource.gerrit.plugins.eventseiffel.config.EventsFilter;
 import com.googlesource.gerrit.plugins.eventseiffel.parsing.EiffelEventParsingQueue;
 
 public class RefUpdateListener implements GitReferenceUpdatedListener {
 
   private final EiffelEventParsingQueue queue;
   private final Provider<EventListenersConfig> configProvider;
+  private final Provider<EventsFilter> eventsFilter;
 
   @Inject
   public RefUpdateListener(
-      EiffelEventParsingQueue queue, Provider<EventListenersConfig> configProvider) {
+      EiffelEventParsingQueue queue,
+      Provider<EventListenersConfig> configProvider,
+      Provider<EventsFilter> eventsFilter) {
     this.queue = queue;
     this.configProvider = configProvider;
+    this.eventsFilter = eventsFilter;
   }
 
   @Override
@@ -39,8 +44,10 @@
       return;
     }
 
-    if (config.refIsBlocked(event.getRefName())
-        || config.projectIsBlocked(event.getProjectName())) {
+    EventsFilter filter = eventsFilter.get();
+
+    if (filter.refIsBlocked(event.getRefName())
+        || filter.projectIsBlocked(event.getProjectName())) {
       return;
     }
     /* The reference was not deleted. */
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParserImpl.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParserImpl.java
index 9a089d4..4e71eb1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParserImpl.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParserImpl.java
@@ -32,8 +32,10 @@
 import com.google.gerrit.extensions.events.RevisionCreatedListener.Event;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.googlesource.gerrit.plugins.eventseiffel.EiffelEventHub;
 import com.googlesource.gerrit.plugins.eventseiffel.cache.EiffelEventIdLookupException;
+import com.googlesource.gerrit.plugins.eventseiffel.config.EventsFilter;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.CompositionDefinedEventKey;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.EventKey;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.SourceChangeEventKey;
@@ -57,6 +59,8 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.revwalk.RevWalkUtils;
 
 /** Creates and pushes missing Eiffel events to the Eiffel event queue. */
 public class EiffelEventParserImpl implements EiffelEventParser {
@@ -68,17 +72,20 @@
   private final GitRepositoryManager repoManager;
   private final UnprocessedCommitsWalker.Factory walkerFactory;
   private final EiffelEventMapper mapper;
+  private final Provider<EventsFilter> eventsFilter;
 
   @Inject
   public EiffelEventParserImpl(
       EiffelEventHub eventQueue,
       GitRepositoryManager repoManager,
       EiffelEventMapper mapper,
-      UnprocessedCommitsWalker.Factory walkerFactory) {
+      UnprocessedCommitsWalker.Factory walkerFactory,
+      Provider<EventsFilter> eventsFilter) {
     this.eventHub = eventQueue;
     this.repoManager = repoManager;
     this.walkerFactory = walkerFactory;
     this.mapper = mapper;
+    this.eventsFilter = eventsFilter;
   }
 
   /* (non-Javadoc)
@@ -272,67 +279,116 @@
   }
 
   private void createAndScheduleCd(
-      String repoName, String tagName, Long creationTime, boolean force)
+      String projectName, String tagName, Long creationTime, boolean force)
       throws EventParsingException {
-    SourceChangeEventKey scs = null;
     Optional<UUID> scsId = Optional.empty();
+    List<Ref> refs = null;
 
     try {
-      String commitId = peelTag(repoName, tagName);
-      scs = SourceChangeEventKey.scsKey(repoName, RefNames.REFS_HEADS + "master", commitId);
+      EventsFilter filter = eventsFilter.get();
+      ObjectId objectId = peelTag(projectName, tagName, filter.blockLightWeightTags());
+      if (objectId == null) {
+        return;
+      }
+      String commitId = objectId.getName();
+
+      /* Check if an event for commit~master has been created. */
+      SourceChangeEventKey scs =
+          SourceChangeEventKey.scsKey(projectName, RefNames.REFS_HEADS + "master", commitId);
       scsId = eventHub.getExistingId(scs);
 
       if (scsId.isEmpty()) {
+        /* No event created for commit~master. Check if event is created for any
+        of the other branches. */
         Retryer<Optional<UUID>> retryer =
             RetryerBuilder.<Optional<UUID>>newBuilder()
                 .retryIfResult(Optional::isEmpty)
                 .withWaitStrategy(WaitStrategies.fixedWait(10, TimeUnit.SECONDS))
                 .withStopStrategy(StopStrategies.stopAfterAttempt(2))
                 .build();
+        try (Repository repo = repoManager.openRepository(Project.nameKey(projectName))) {
+          refs =
+              repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_HEADS).stream()
+                  .collect(Collectors.toList());
+        } catch (IOException e) {
+          throw new EventParsingException(
+              e, "Unable to get branches for: %s:%s", projectName, tagName);
+        }
         try {
-          scsId = retryer.call(() -> findSourceChangeEventKey(repoName, commitId));
+          List<String> branches =
+              refs.stream()
+                  .map(Ref::getName)
+                  .filter(branch -> !branch.equals(RefNames.REFS_HEADS + "master"))
+                  .collect(Collectors.toList());
+          scsId = retryer.call(() -> findSourceChangeEventKey(projectName, commitId, branches));
         } catch (RetryException | ExecutionException e) {
-          throw new EventParsingException(e, "Failed to find SCS for %s in %s", commitId, repoName);
+          logger.atWarning().withCause(e).log(
+              "Failed to find SCS for %s in %s when trying to create CD for tag %s",
+              commitId, projectName, tagName);
         }
       }
+
       if (scsId.isEmpty()) {
-        throw new EventParsingException("Could not find SCS for: %s in %s", commitId, repoName);
+        /* No event has been created for the commit. Find any non-blocked branch that
+        commit is merged into and create SCS events for that branch. */
+        List<Ref> branches;
+        try (Repository repo = repoManager.openRepository(Project.nameKey(projectName))) {
+          RevWalk rw = new RevWalk(repo);
+          refs =
+              refs.stream()
+                  .filter(ref -> !filter.refIsBlocked(ref.getName()))
+                  .collect(Collectors.toList());
+          branches = RevWalkUtils.findBranchesReachableFrom(rw.parseCommit(objectId), rw, refs);
+        } catch (IOException e) {
+          throw new EventParsingException(
+              e, "Unable to get reachable branches for: %s:%s", projectName, tagName);
+        }
+        if (branches.isEmpty()) {
+          throw new EventParsingException(
+              "Could not find any unblocked branch for SCS with: %s in %s so CD could not be created for tag %s",
+              commitId, projectName, tagName);
+        }
+        String branch = branches.get(0).getName();
+        scs = SourceChangeEventKey.scsKey(projectName, branch, commitId);
+        createAndScheduleMissingScss(scs, null, null, null);
+        scsId = eventHub.getExistingId(scs);
       }
-      pushToHub(mapper.toCd(repoName, tagName, creationTime, scsId.get()), force);
+
+      if (scsId.isEmpty()) {
+        throw new EventParsingException(
+            "Could not find or create SCS for %s in %s so CD could not be created for tag %s",
+            commitId, projectName, tagName);
+      }
+      pushToHub(mapper.toCd(projectName, tagName, creationTime, scsId.get()), force);
     } catch (EiffelEventIdLookupException | InterruptedException e) {
       throw new EventParsingException(
           e,
           "Event creation failed for: %s",
-          CompositionDefinedEventKey.create(mapper.tagCompositionName(repoName), tagName));
+          CompositionDefinedEventKey.create(mapper.tagCompositionName(projectName), tagName));
     }
   }
 
-  private String peelTag(String repoName, String tagName) throws EventParsingException {
-    try (Repository repo = repoManager.openRepository(Project.nameKey(repoName))) {
+  private ObjectId peelTag(String projectName, String tagName, boolean blockLightWeightTags)
+      throws EventParsingException {
+    try (Repository repo = repoManager.openRepository(Project.nameKey(projectName))) {
       Ref tagRef = repo.getRefDatabase().exactRef(Constants.R_TAGS + tagName);
       if (tagRef != null) {
         ObjectId peeled = repo.getRefDatabase().peel(tagRef).getPeeledObjectId();
-        return peeled != null ? peeled.getName() : tagRef.getObjectId().getName();
+        if (peeled != null) return peeled;
+        if (!blockLightWeightTags) return tagRef.getObjectId();
+        logger.atInfo().log("Creation of CD is blocked for lightweight tags");
+        return null;
       }
-      throw new EventParsingException("Cannot find tag: %s:%s", repoName, tagName);
+      throw new EventParsingException("Cannot find tag: %s:%s", projectName, tagName);
     } catch (IOException e) {
-      throw new EventParsingException(e, "Unable to peel tag: %s:%s", repoName, tagName);
+      throw new EventParsingException(e, "Unable to peel tag: %s:%s", projectName, tagName);
     }
   }
 
-  private Optional<UUID> findSourceChangeEventKey(String repoName, String commitId)
+  private Optional<UUID> findSourceChangeEventKey(
+      String projectName, String commitId, List<String> branches)
       throws EiffelEventIdLookupException {
-    List<String> branches;
-    try (Repository repo = repoManager.openRepository(Project.nameKey(repoName))) {
-      branches =
-          repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_HEADS).stream()
-              .map(Ref::getName)
-              .collect(Collectors.toList());
-    } catch (IOException ioe) {
-      logger.atSevere().withCause(ioe).log("Unable to find branches of %s.", repoName);
-      return Optional.empty();
-    }
-    return eventHub.getScsForCommit(repoName, commitId, branches);
+    return eventHub.getScsForCommit(projectName, commitId, branches);
   }
 
   private void pushToHub(EiffelEvent toPush) throws InterruptedException {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/rest/EventCreationResponse.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/rest/EventCreationResponse.java
index 8f43ba9..8303940 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/rest/EventCreationResponse.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/rest/EventCreationResponse.java
@@ -43,7 +43,7 @@
   }
 
   static EventCreationResponse artc(TagResource res) {
-    return new EventCreationResponse(res, null, ARTC, CD);
+    return new EventCreationResponse(res, null, ARTC, CD, SCS, SCC);
   }
 
   private final EventCreationScheduled scheduled;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/rest/RestModule.java b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/rest/RestModule.java
index 29a643e..637f764 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/rest/RestModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/eventseiffel/rest/RestModule.java
@@ -18,11 +18,16 @@
 import static com.google.gerrit.server.project.TagResource.TAG_KIND;
 
 import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.inject.Scopes;
+import com.googlesource.gerrit.plugins.eventseiffel.config.EventListenersConfig;
 
 public class RestModule extends RestApiModule {
 
   @Override
   public void configure() {
+    bind(EventListenersConfig.class)
+        .toProvider(EventListenersConfig.Provider.class)
+        .in(Scopes.SINGLETON);
     post(BRANCH_KIND, "createSccs").to(CreateSccs.class);
     post(BRANCH_KIND, "createScss").to(CreateScss.class);
     post(TAG_KIND, "createArtcs").to(CreateArtcs.class);
diff --git a/src/main/resources/Documentation/rest-api-events-eiffel-artc-post.md b/src/main/resources/Documentation/rest-api-events-eiffel-artc-post.md
new file mode 100644
index 0000000..67189f1
--- /dev/null
+++ b/src/main/resources/Documentation/rest-api-events-eiffel-artc-post.md
@@ -0,0 +1,70 @@
+POST createArtcs
+==============================
+
+```
+POST /projects/{project-name}/tags/{tag-name}/@PLUGIN@~createArtcs
+```
+
+DESCRIPTION
+-----------
+Creates missing
+[ArtifactCreatedEvent(ARTC)](https://github.com/eiffel-community/eiffel/blob/edition-paris/eiffel-vocabulary/EiffelArtifactCreatedEvent.md)
+,
+[CompositionDefinedEvent(CD)](https://github.com/eiffel-community/eiffel/blob/edition-paris/eiffel-vocabulary/EiffelCompositionDefinedEvent.md)
+,
+[SourceChangeSubmitted(SCS)](https://github.com/eiffel-community/eiffel/blob/edition-paris/eiffel-vocabulary/EiffelSourceChangeSubmittedEvent.md)
+and
+[SourceChangeCreated(SCC)](https://github.com/eiffel-community/eiffel/blob/edition-paris/eiffel-vocabulary/EiffelSourceChangeCreatedEvent.md)
+events for commit pointed by {tag-name}.
+
+ACCESS
+------
+[Administrate Server](../../../Documentation/access-control.html#capability_administrateServer)
+capability is required.
+
+EXAMPLES
+--------
+
+#### Create missing ARTC events for the tag stable-3.5 of project my/project: ####
+
+```
+  curl -X POST --user janeadmin:secret \
+    -H "content-type:application/json" \
+    http://host:port/a/projects/my%2Fproject/tags/stable-3.5/@PLUGIN@~createArtcs
+```
+
+__Response:__
+
+```
+  )]}'
+  {
+   "types": [
+     "EiffelArtifactCreatedEvent",
+     "EiffelCompositionDefinedEvent",
+     "EiffelSourceChangeCreatedEvent",
+     "EiffelSourceChangeSubmittedEvent",
+   ],
+   "repo_name": "my/project",
+   "ref": "refs/tags/stable-3.5",
+   "status": "Event creation scheduled",
+  }
+```
+
+### Response object ###
+types
+: The types of events that may be created.
+
+repo_name
+: Name of the repository.
+
+ref
+: The ref of the tag from which we will create an ARTC event.
+
+SEE ALSO
+--------
+* [Plugin Development](../../../Documentation/dev-plugins.html)
+* [REST API Protocol Details](../../../Documentation/rest-api.html#_protocol_details)
+
+GERRIT
+------
+Part of [Gerrit Code Review](../../../Documentation/index.html)
diff --git a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/EiffelEventsTest.java b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/EiffelEventsTest.java
index c847bb0..0433b7a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/EiffelEventsTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/EiffelEventsTest.java
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.eventseiffel;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.plugins.eventseiffel.config.EventsFilter.Provider.toCombinedMatcher;
 import static com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelEventType.SCC;
 import static com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelEventType.SCS;
 
@@ -24,9 +25,11 @@
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.TagInput;
 import com.google.inject.Provider;
+import com.googlesource.gerrit.plugins.eventseiffel.config.EventsFilter;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.EventKey;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.SourceChangeEventKey;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.api.EventStorageException;
+import com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelEventType;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelLinkInfo;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelLinkType;
 import com.googlesource.gerrit.plugins.eventseiffel.mapping.EiffelEventFactory;
@@ -115,6 +118,22 @@
         result.getCommit().getName());
   }
 
+  protected void setScsHandled() throws Exception {
+    markAsHandled(eventForHead(EiffelEventType.SCS), getHead(repo(), "HEAD"));
+  }
+
+  protected SourceChangeEventKey eventForHead(EiffelEventType type) throws Exception {
+    return SourceChangeEventKey.create(project.get(), getHead(), getHeadRevision(), type);
+  }
+
+  protected String getHead() throws Exception {
+    return gApi.projects().name(project.get()).head();
+  }
+
+  protected String getHeadRevision() throws Exception {
+    return getHead(repo(), "HEAD").getName();
+  }
+
   protected UUID markAsHandled(EventKey key) throws EventStorageException {
     Optional<UUID> eventId = TestEventStorage.INSTANCE.getEventId(key);
     if (eventId.isPresent()) {
@@ -184,6 +203,39 @@
     return gApi.projects().name(project.get()).tag(input.ref).create(input).get().ref;
   }
 
+  public static class TestEventsFilterProvider implements Provider<EventsFilter> {
+    private static String[] refPatterns;
+    private static String[] projectPatterns;
+    private static boolean blockLightweightTags;
+
+    static {
+      reset();
+    }
+
+    public static void reset() {
+      refPatterns = projectPatterns = new String[0];
+      blockLightweightTags = false;
+    }
+
+    public static void setBlockedRefPatterns(String... patterns) {
+      refPatterns = patterns;
+    }
+
+    public static void setBlockedProjectPatterns(String... patterns) {
+      projectPatterns = patterns;
+    }
+
+    public static void blockLightWeightTags() {
+      blockLightweightTags = true;
+    }
+
+    @Override
+    public EventsFilter get() {
+      return new EventsFilter(
+          toCombinedMatcher(projectPatterns), toCombinedMatcher(refPatterns), blockLightweightTags);
+    }
+  }
+
   public static class TestEventFactoryProvider implements Provider<EiffelEventFactory> {
 
     @Override
diff --git a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/EiffelEventsTestModule.java b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/EiffelEventsTestModule.java
index 17b5acd..345753d 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/EiffelEventsTestModule.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/EiffelEventsTestModule.java
@@ -20,9 +20,11 @@
 import com.google.inject.Singleton;
 import com.google.inject.internal.UniqueAnnotations;
 import com.googlesource.gerrit.plugins.eventseiffel.EiffelEventsTest.TestEventFactoryProvider;
+import com.googlesource.gerrit.plugins.eventseiffel.EiffelEventsTest.TestEventsFilterProvider;
 import com.googlesource.gerrit.plugins.eventseiffel.cache.EiffelEventIdCacheImpl;
 import com.googlesource.gerrit.plugins.eventseiffel.config.EventIdCacheConfig;
 import com.googlesource.gerrit.plugins.eventseiffel.config.EventMappingConfig;
+import com.googlesource.gerrit.plugins.eventseiffel.config.EventsFilter;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.api.EventStorage;
 import com.googlesource.gerrit.plugins.eventseiffel.mapping.EiffelEventFactory;
 import com.googlesource.gerrit.plugins.eventseiffel.parsing.EiffelEventParser;
@@ -44,6 +46,8 @@
     bind(EiffelEventParsingExecutor.class).to(EiffelEventParsingExecutor.Direct.class);
     bind(TestEventPublisher.class).in(Scopes.SINGLETON);
     bind(EiffelEventHub.Consumer.class).to(TestEventPublisher.class);
+
+    bind(EventsFilter.class).toProvider(TestEventsFilterProvider.class);
     bind(EiffelEventParser.class).to(EiffelEventParserImpl.class);
     bind(EiffelEventParsingQueue.class);
     bind(EiffelEventFactory.class).toProvider(TestEventFactoryProvider.class);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/config/EventListenersConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/config/EventsFilterTest.java
similarity index 66%
rename from src/test/java/com/googlesource/gerrit/plugins/eventseiffel/config/EventListenersConfigTest.java
rename to src/test/java/com/googlesource/gerrit/plugins/eventseiffel/config/EventsFilterTest.java
index ab2ca17..268cc2c 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/config/EventListenersConfigTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/config/EventsFilterTest.java
@@ -14,9 +14,9 @@
 
 package com.googlesource.gerrit.plugins.eventseiffel.config;
 
-import static com.googlesource.gerrit.plugins.eventseiffel.config.EventListenersConfig.Provider.filterMatches;
-import static com.googlesource.gerrit.plugins.eventseiffel.config.EventListenersConfig.Provider.fromConfig;
-import static com.googlesource.gerrit.plugins.eventseiffel.config.EventListenersConfig.Provider.toCombinedMatcher;
+import static com.googlesource.gerrit.plugins.eventseiffel.config.EventsFilter.Provider.filterMatches;
+import static com.googlesource.gerrit.plugins.eventseiffel.config.EventsFilter.Provider.fromConfig;
+import static com.googlesource.gerrit.plugins.eventseiffel.config.EventsFilter.Provider.toCombinedMatcher;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
@@ -24,51 +24,51 @@
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
-public class EventListenersConfigTest {
+public class EventsFilterTest {
 
   @Test
   public void regexBlockRefPattern() throws Exception {
-    EventListenersConfig cfg = refsPatterns("^refs/.*/blocked");
-    assertTrue(cfg.refIsBlocked("refs/that/are/blocked"));
-    assertFalse(cfg.refIsBlocked("refs/allowed"));
+    EventsFilter filter = refsPatterns("^refs/.*/blocked");
+    assertTrue(filter.refIsBlocked("refs/that/are/blocked"));
+    assertFalse(filter.refIsBlocked("refs/allowed"));
 
-    cfg = refsPatterns("^refs/(heads|tags)");
-    assertTrue(cfg.refIsBlocked("refs/heads"));
-    assertTrue(cfg.refIsBlocked("refs/tags"));
-    assertFalse(cfg.refIsBlocked("refs/meta"));
+    filter = refsPatterns("^refs/(heads|tags)");
+    assertTrue(filter.refIsBlocked("refs/heads"));
+    assertTrue(filter.refIsBlocked("refs/tags"));
+    assertFalse(filter.refIsBlocked("refs/meta"));
   }
 
   @Test
   public void wildcardBlockRefPattern() throws Exception {
-    EventListenersConfig cfg = refsPatterns("");
-    assertFalse(cfg.refIsBlocked("refs/blocked/always"));
+    EventsFilter filter = refsPatterns("");
+    assertFalse(filter.refIsBlocked("refs/blocked/always"));
 
-    cfg = refsPatterns("refs/blocked/*");
-    assertTrue(cfg.refIsBlocked("refs/blocked/always"));
-    assertFalse(cfg.refIsBlocked("refs/allowed/always"));
+    filter = refsPatterns("refs/blocked/*");
+    assertTrue(filter.refIsBlocked("refs/blocked/always"));
+    assertFalse(filter.refIsBlocked("refs/allowed/always"));
   }
 
   @Test
   public void exactBlockRefPattern() throws Exception {
-    EventListenersConfig cfg = refsPatterns("");
-    assertFalse(cfg.refIsBlocked("refs/blocked/exact"));
+    EventsFilter filter = refsPatterns("");
+    assertFalse(filter.refIsBlocked("refs/blocked/exact"));
 
-    cfg = refsPatterns("refs/blocked/exact");
-    assertTrue(cfg.refIsBlocked("refs/blocked/exact"));
-    assertFalse(cfg.refIsBlocked("refs/blocked/exact2"));
+    filter = refsPatterns("refs/blocked/exact");
+    assertTrue(filter.refIsBlocked("refs/blocked/exact"));
+    assertFalse(filter.refIsBlocked("refs/blocked/exact2"));
   }
 
   @Test
   public void combinedBlockRefPattern() throws Exception {
-    EventListenersConfig cfg = refsPatterns("");
-    assertFalse(cfg.refIsBlocked("refs/certainly/blocked"));
-    assertFalse(cfg.refIsBlocked("refs/blocked/always"));
-    assertFalse(cfg.refIsBlocked("refs/not/allowed/at/all"));
+    EventsFilter filter = refsPatterns("");
+    assertFalse(filter.refIsBlocked("refs/certainly/blocked"));
+    assertFalse(filter.refIsBlocked("refs/blocked/always"));
+    assertFalse(filter.refIsBlocked("refs/not/allowed/at/all"));
 
-    cfg = refsPatterns("refs/blocked/*", "^refs/.*/blocked", "refs/not/allowed/at/all");
-    assertTrue(cfg.refIsBlocked("refs/certainly/blocked"));
-    assertTrue(cfg.refIsBlocked("refs/blocked/always"));
-    assertTrue(cfg.refIsBlocked("refs/not/allowed/at/all"));
+    filter = refsPatterns("refs/blocked/*", "^refs/.*/blocked", "refs/not/allowed/at/all");
+    assertTrue(filter.refIsBlocked("refs/certainly/blocked"));
+    assertTrue(filter.refIsBlocked("refs/blocked/always"));
+    assertTrue(filter.refIsBlocked("refs/not/allowed/at/all"));
   }
 
   @Test
@@ -132,26 +132,25 @@
   public void configParsedCorrectly() throws Exception {
     Config cfg = new Config();
     cfg.fromText(
-        "[EventListeners]\n"
+        "[EventsFilter]\n"
             + "blockedRef = refs/heads/blocked\n"
             + "blockedRef = refs/heads/also/blocked\n"
             + "blockedProject = blocked/project\n"
             + "blockedProject = blocked/projects/*\n"
             + "enabled = False\n");
-    EventListenersConfig fromText = fromConfig(cfg);
+    EventsFilter fromText = fromConfig(cfg);
 
     assertTrue(fromText.refIsBlocked("refs/heads/blocked"));
     assertTrue(fromText.refIsBlocked("refs/heads/also/blocked"));
     assertTrue(fromText.projectIsBlocked("blocked/project"));
     assertTrue(fromText.projectIsBlocked("blocked/projects/something"));
-    assertFalse(fromText.isEnabled());
   }
 
-  private EventListenersConfig refsPatterns(String... patterns) {
-    return new EventListenersConfig(true, toCombinedMatcher(), toCombinedMatcher(patterns));
+  private EventsFilter refsPatterns(String... patterns) {
+    return new EventsFilter(toCombinedMatcher(), toCombinedMatcher(patterns), false);
   }
 
-  private EventListenersConfig projectsPatterns(String... patterns) {
-    return new EventListenersConfig(true, toCombinedMatcher(patterns), toCombinedMatcher());
+  private EventsFilter projectsPatterns(String... patterns) {
+    return new EventsFilter(toCombinedMatcher(patterns), toCombinedMatcher(), false);
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/GerritEventListenersIT.java b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/GerritEventListenersIT.java
index 126eb50..0527b19 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/GerritEventListenersIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/listeners/GerritEventListenersIT.java
@@ -14,7 +14,6 @@
 
 package com.googlesource.gerrit.plugins.eventseiffel.listeners;
 
-import static com.googlesource.gerrit.plugins.eventseiffel.config.EventListenersConfig.Provider.toCombinedMatcher;
 import static com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelEventType.SCC;
 import static com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelEventType.SCS;
 import static org.junit.Assert.assertEquals;
@@ -38,6 +37,7 @@
 import com.googlesource.gerrit.plugins.eventseiffel.TestEventPublisher;
 import com.googlesource.gerrit.plugins.eventseiffel.config.EiffelConfig;
 import com.googlesource.gerrit.plugins.eventseiffel.config.EventListenersConfig;
+import com.googlesource.gerrit.plugins.eventseiffel.config.EventsFilter;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.ArtifactEventKey;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.CompositionDefinedEventKey;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.EventKey;
@@ -64,7 +64,8 @@
   @Before
   public void before() {
     publisher = plugin.getSysInjector().getInstance(TestEventPublisher.class);
-    TestEventListenerConfigProvider.reset();
+    TestEventsFilterProvider.reset();
+    TestEventListenersConfigProvider.reset();
   }
 
   @After
@@ -111,13 +112,13 @@
     UUID masterSccEventId = markMasterAsHandled(SCC);
     RevCommit previousMaster = getMaster();
 
-    TestEventListenerConfigProvider.disable();
+    TestEventListenersConfigProvider.disable();
 
     PushOneCommit.Result res1 = createChange();
     SourceChangeEventKey sccKey1 = toSccKey(res1);
     merge(res1);
 
-    TestEventListenerConfigProvider.enable();
+    TestEventListenersConfigProvider.enable();
 
     PushOneCommit.Result res2 = createChange();
     SourceChangeEventKey sccKey2 = toSccKey(res2);
@@ -163,7 +164,7 @@
 
   @Test
   public void noEventsCreatedWhenDisabled() throws Exception {
-    TestEventListenerConfigProvider.disable();
+    TestEventListenersConfigProvider.disable();
     PushOneCommit.Result res = createChange();
     SourceChangeEventKey sccKey = toSccKey(res);
     EiffelEvent sccEvent = publisher.getPublished(sccKey);
@@ -175,7 +176,7 @@
 
   @Test
   public void noEventsCreatedWhenRefIsBlocked() throws Exception {
-    TestEventListenerConfigProvider.setBlockedRefPatterns("refs/heads/master");
+    TestEventsFilterProvider.setBlockedRefPatterns("refs/heads/master");
     PushOneCommit.Result res = createChange();
     SourceChangeEventKey sccKey = toSccKey(res);
     EiffelEvent sccEvent = publisher.getPublished(sccKey);
@@ -188,7 +189,7 @@
 
   @Test
   public void noEventsCreatedWhenProjectIsBlocked() throws Exception {
-    TestEventListenerConfigProvider.setBlockedProjectPatterns(project.get());
+    TestEventsFilterProvider.setBlockedProjectPatterns(project.get());
     PushOneCommit.Result res = createChange();
     SourceChangeEventKey sccKey = toSccKey(res);
     EiffelEvent sccEvent = publisher.getPublished(sccKey);
@@ -200,8 +201,16 @@
 
   @Test
   public void lightweightTagCreatedResultsInEvent() throws Exception {
-    UUID parentEventId = markMasterAsHandled(SCS);
-    String tagName = createTagRef(false).substring(RefNames.REFS_TAGS.length());
+    tagCreatedResultsInEvent(false);
+  }
+
+  @Test
+  public void annotatedTagCreatedResultsInEvent() throws Exception {
+    tagCreatedResultsInEvent(true);
+  }
+
+  private void tagCreatedResultsInEvent(boolean annotated) throws Exception {
+    String tagName = createTagRef(annotated).substring(RefNames.REFS_TAGS.length());
 
     EventKey artcKey = ArtifactEventKey.create(tagPURL(project.get(), tagName));
     EiffelEvent artcEvent = publisher.getPublished(artcKey);
@@ -213,13 +222,37 @@
     assertNotNull("Publisher did not find CD event", cdEvent);
     assertEquals(EventKey.fromEvent(cdEvent), cdKey);
 
+    EventKey scsKey = SourceChangeEventKey.scsKey(project.get(), "master", getMaster());
+    EiffelEvent scsEvent = publisher.getPublished(scsKey);
+    assertNotNull("Publisher did not find SCS event", scsEvent);
+    assertEquals(EventKey.fromEvent(scsEvent), scsKey);
+
     assertArtcLinks(cdEvent.meta.id, artcEvent.links);
-    assertCdLinks(parentEventId, cdEvent.links);
+    assertCdLinks(scsEvent.meta.id, cdEvent.links);
   }
 
   @Test
-  public void annotatedTagCreatedResultsInEvent() throws Exception {
+  public void tagCreatedResultsInNoEventWhenBranchIsBlocked() throws Exception {
+    TestEventsFilterProvider.setBlockedRefPatterns("refs/heads/master");
+    String tagName = createTagRef(true).substring(RefNames.REFS_TAGS.length());
+
+    EventKey artcKey = ArtifactEventKey.create(tagPURL(project.get(), tagName));
+    EiffelEvent artcEvent = publisher.getPublished(artcKey);
+    assertNull("Publisher found ARTC event", artcEvent);
+
+    EventKey cdKey = CompositionDefinedEventKey.create(tagCompositionName(project.get()), tagName);
+    EiffelEvent cdEvent = publisher.getPublished(cdKey);
+    assertNull("Publisher found CD event", cdEvent);
+
+    EventKey scsKey = SourceChangeEventKey.scsKey(project.get(), "master", getMaster());
+    EiffelEvent scsEvent = publisher.getPublished(scsKey);
+    assertNull("Publisher found SCS event", scsEvent);
+  }
+
+  @Test
+  public void tagCreatedResultsInNoEventWhenBranchIsBlockedSCSHandled() throws Exception {
     UUID parentEventId = markMasterAsHandled(SCS);
+    TestEventsFilterProvider.setBlockedRefPatterns("refs/heads/master");
     String tagName = createTagRef(true).substring(RefNames.REFS_TAGS.length());
 
     EventKey artcKey = ArtifactEventKey.create(tagPURL(project.get(), tagName));
@@ -275,10 +308,8 @@
     return originMaster;
   }
 
-  public static class TestEventListenerConfigProvider implements Provider<EventListenersConfig> {
+  public static class TestEventListenersConfigProvider implements Provider<EventListenersConfig> {
     private static boolean enabled;
-    private static String[] refPatterns;
-    private static String[] projectPatterns;
 
     static {
       reset();
@@ -286,15 +317,6 @@
 
     static void reset() {
       enabled = true;
-      refPatterns = projectPatterns = new String[0];
-    }
-
-    static void setBlockedRefPatterns(String... patterns) {
-      refPatterns = patterns;
-    }
-
-    static void setBlockedProjectPatterns(String... patterns) {
-      projectPatterns = patterns;
     }
 
     static void disable() {
@@ -307,8 +329,7 @@
 
     @Override
     public EventListenersConfig get() {
-      return new EventListenersConfig(
-          enabled, toCombinedMatcher(projectPatterns), toCombinedMatcher(refPatterns));
+      return new EventListenersConfig(enabled);
     }
   }
 
@@ -317,7 +338,8 @@
     @Override
     protected void configure() {
       install(new EiffelEventsTestModule());
-      bind(EventListenersConfig.class).toProvider(TestEventListenerConfigProvider.class);
+      bind(EventListenersConfig.class).toProvider(TestEventListenersConfigProvider.class);
+      bind(EventsFilter.class).toProvider(TestEventsFilterProvider.class);
       DynamicSet.bind(binder(), RevisionCreatedListener.class).to(PatchsetCreatedListener.class);
       DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(RefUpdateListener.class);
       bind(EiffelConfig.class).toProvider(EiffelConfig.Provider.class).in(Scopes.SINGLETON);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParserIT.java b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParserIT.java
index 668bb8e..e11ee3d 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParserIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParserIT.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.config.GlobalPluginConfig;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.googlesource.gerrit.plugins.eventseiffel.TestEventHub;
 import com.googlesource.gerrit.plugins.eventseiffel.TestEventStorage;
@@ -48,6 +49,7 @@
   public void setUp() {
     eventParser = plugin.getSysInjector().getInstance(EiffelEventParserImpl.class);
     TestEventHub.EVENTS.clear();
+    TestEventsFilterProvider.reset();
   }
 
   @Test
@@ -225,14 +227,66 @@
   }
 
   @Test
-  public void artcNotCreatedWhenMissingScs() throws Exception {
-    String tag =
+  public void artcQueued() throws Exception {
+    String ref =
+        createTagRef(getHead(repo(), "HEAD").getName(), true).substring(Constants.R_TAGS.length());
+
+    SourceChangeEventKey scc =
+        SourceChangeEventKey.sccKey(project.get(), getHead(), getHeadRevision());
+    SourceChangeEventKey scs = scc.copy(SCS);
+    ArtifactEventKey artc = ArtifactEventKey.create(tagPURL(project.get(), ref, "localhost"));
+    CompositionDefinedEventKey cd =
+        CompositionDefinedEventKey.create(tagCompositionName(project.get(), "localhost"), ref);
+
+    eventParser.createAndScheduleArtc(project.get(), ref, EPOCH_MILLIS, false);
+    assertEquals(4, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, scc);
+    assertCorrectEvent(1, scs);
+    assertCorrectEvent(2, cd);
+    assertCorrectEvent(3, artc);
+  }
+
+  @Test
+  public void artcQueuedBlockBranch() throws Exception {
+    TestEventsFilterProvider.setBlockedRefPatterns("refs/heads/master");
+    String ref =
         createTagRef(getHead(repo(), "HEAD").getName(), true).substring(Constants.R_TAGS.length());
     EventParsingException thrown =
         assertThrows(
             EventParsingException.class,
-            () -> eventParser.createAndScheduleArtc(project.get(), tag, EPOCH_MILLIS, false));
-    assertThat(thrown).hasMessageThat().contains("Failed to find SCS for");
+            () -> eventParser.createAndScheduleArtc(project.get(), ref, EPOCH_MILLIS, false));
+    assertThat(thrown).hasMessageThat().startsWith("Could not find any unblocked branch for SCS");
+  }
+
+  @Test
+  public void artcQueuedBlockBranchButReachableFromOtherBranch() throws Exception {
+    TestEventsFilterProvider.setBlockedRefPatterns("refs/heads/master");
+    createBranch(BranchNameKey.create(project, "other-branch"));
+    String ref =
+        createTagRef(getHead(repo(), "HEAD").getName(), true).substring(Constants.R_TAGS.length());
+
+    SourceChangeEventKey scc =
+        SourceChangeEventKey.sccKey(project.get(), "other-branch", getHeadRevision());
+    SourceChangeEventKey scs = scc.copy(SCS);
+    ArtifactEventKey artc = ArtifactEventKey.create(tagPURL(project.get(), ref, "localhost"));
+    CompositionDefinedEventKey cd =
+        CompositionDefinedEventKey.create(tagCompositionName(project.get(), "localhost"), ref);
+
+    eventParser.createAndScheduleArtc(project.get(), ref, EPOCH_MILLIS, false);
+    assertEquals(4, TestEventHub.EVENTS.size());
+    assertCorrectEvent(0, scc);
+    assertCorrectEvent(1, scs);
+    assertCorrectEvent(2, cd);
+    assertCorrectEvent(3, artc);
+  }
+
+  @Test
+  public void artcQueuedNoTagCreated() throws Exception {
+    EventParsingException thrown =
+        assertThrows(
+            EventParsingException.class,
+            () -> eventParser.createAndScheduleArtc(project.get(), TAG_NAME, EPOCH_MILLIS, false));
+    assertThat(thrown).hasMessageThat().startsWith("Cannot find tag");
   }
 
   @Test
@@ -246,6 +300,23 @@
   }
 
   @Test
+  public void annotatedTagArtcQueuedscsHandledLightWeightBlocked() throws Exception {
+    TestEventsFilterProvider.blockLightWeightTags();
+    assertArtcQueuedScsHandled(true);
+  }
+
+  @Test
+  public void lightWeightTagArtcQueuedscsHandledLightWeightBlocked() throws Exception {
+    setScsHandled();
+    TestEventsFilterProvider.blockLightWeightTags();
+    String ref =
+        createTagRef(getHead(repo(), "HEAD").getName(), false).substring(Constants.R_TAGS.length());
+
+    eventParser.createAndScheduleArtc(project.get(), ref, EPOCH_MILLIS, false);
+    assertEquals(0, TestEventHub.EVENTS.size());
+  }
+
+  @Test
   public void artcQueuedCdHandled() throws Exception {
     String ref = setCdHandled();
     eventParser.createAndScheduleArtc(project.get(), ref, EPOCH_MILLIS, false);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParsingTest.java b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParsingTest.java
index a0e8d8a..a606e14 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParsingTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/EiffelEventParsingTest.java
@@ -5,8 +5,6 @@
 import com.googlesource.gerrit.plugins.eventseiffel.EiffelEventsTest;
 import com.googlesource.gerrit.plugins.eventseiffel.TestEventHub;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.EventKey;
-import com.googlesource.gerrit.plugins.eventseiffel.eiffel.SourceChangeEventKey;
-import com.googlesource.gerrit.plugins.eventseiffel.eiffel.dto.EiffelEventType;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import org.junit.Ignore;
@@ -41,20 +39,4 @@
     EventKey actual = EventKey.fromEvent(TestEventHub.EVENTS.get(order));
     assertEquals(expected, actual);
   }
-
-  protected void setScsHandled() throws Exception {
-    markAsHandled(eventForHead(EiffelEventType.SCS), getHead(repo(), "HEAD"));
-  }
-
-  protected SourceChangeEventKey eventForHead(EiffelEventType type) throws Exception {
-    return SourceChangeEventKey.create(project.get(), getHead(), getHeadRevision(), type);
-  }
-
-  protected String getHead() throws Exception {
-    return gApi.projects().name(project.get()).head();
-  }
-
-  protected String getHeadRevision() throws Exception {
-    return getHead(repo(), "HEAD").getName();
-  }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/ParsingTestModule.java b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/ParsingTestModule.java
index f3fe7e5..b01640d 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/ParsingTestModule.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/parsing/ParsingTestModule.java
@@ -4,12 +4,14 @@
 import com.google.inject.Scopes;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.eventseiffel.EiffelEventHub;
+import com.googlesource.gerrit.plugins.eventseiffel.EiffelEventsTest.TestEventsFilterProvider;
 import com.googlesource.gerrit.plugins.eventseiffel.TestEventHub;
 import com.googlesource.gerrit.plugins.eventseiffel.TestEventStorage;
 import com.googlesource.gerrit.plugins.eventseiffel.cache.EiffelEventIdCacheImpl;
 import com.googlesource.gerrit.plugins.eventseiffel.config.EiffelConfig;
 import com.googlesource.gerrit.plugins.eventseiffel.config.EventIdCacheConfig;
 import com.googlesource.gerrit.plugins.eventseiffel.config.EventMappingConfig;
+import com.googlesource.gerrit.plugins.eventseiffel.config.EventsFilter;
 import com.googlesource.gerrit.plugins.eventseiffel.eiffel.api.EventStorage;
 import com.googlesource.gerrit.plugins.eventseiffel.mapping.EiffelEventFactory;
 import com.googlesource.gerrit.plugins.eventseiffel.mapping.EiffelEventMapper;
@@ -21,6 +23,7 @@
   protected void configure() {
     bind(EventStorage.class).toProvider(TestEventStorage.Provider.class).in(Singleton.class);
     bind(EventIdCacheConfig.class).toProvider(EventIdCacheConfig.Provider.class);
+    bind(EventsFilter.class).toProvider(TestEventsFilterProvider.class);
     install(EiffelEventIdCacheImpl.module());
     bind(EiffelEventHub.class).to(TestEventHub.class);
     bind(EventMappingConfig.class).toProvider(EventMappingConfig.Provider.class);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/rest/EventsEiffelRestIT.java b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/rest/EventsEiffelRestIT.java
index d483a66..691acf1 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/rest/EventsEiffelRestIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/eventseiffel/rest/EventsEiffelRestIT.java
@@ -123,7 +123,6 @@
 
   @Test
   public void createArtcAsAdmin() throws Exception {
-    createScss("master", true).assertStatus(202);
     String tagName = createTagRef(true).substring(RefNames.REFS_TAGS.length());
     createArtcs(tagName, true).assertStatus(202);
     ArtifactEventKey artc = ArtifactEventKey.create(tagPURL(project.get(), tagName));
@@ -133,7 +132,6 @@
 
   @Test
   public void createArtcAsNonAdminForbidden() throws Exception {
-    createScss("master", true).assertStatus(202);
     String tagName = createTagRef(true).substring(RefNames.REFS_TAGS.length());
     createArtcs(tagName, false).assertStatus(403);
     ArtifactEventKey artc = ArtifactEventKey.create(tagPURL(project.get(), tagName));