Create a version in the ITS based on an event property value

create-version-from-property is a new type of action named ProjectAction.
This type of actions are triggered on the same events but don't require a matching issue
to be executed.

A ProjectAction requires an association to be set between a Gerrit project and
its ITS counterpart in order to know on which ITS project the action
must be performed.

create-version-from-property can be used to create a version in the ITS project
when a Tag is created in the Gerrit project.

Change-Id: Ifcd75e1665df8090ce8e458afc192275265a61c3
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java
index de80229..6c68777 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java
@@ -35,6 +35,7 @@
 import com.googlesource.gerrit.plugins.its.base.workflow.AddSoyComment;
 import com.googlesource.gerrit.plugins.its.base.workflow.AddStandardComment;
 import com.googlesource.gerrit.plugins.its.base.workflow.Condition;
+import com.googlesource.gerrit.plugins.its.base.workflow.CreateVersionFromProperty;
 import com.googlesource.gerrit.plugins.its.base.workflow.ItsRulesProjectCacheImpl;
 import com.googlesource.gerrit.plugins.its.base.workflow.LogEvent;
 import com.googlesource.gerrit.plugins.its.base.workflow.Rule;
@@ -70,6 +71,7 @@
     factory(AddComment.Factory.class);
     factory(AddSoyComment.Factory.class);
     factory(AddStandardComment.Factory.class);
+    factory(CreateVersionFromProperty.Factory.class);
     factory(LogEvent.Factory.class);
     factory(AddPropertyToField.Factory.class);
     install(ItsRulesProjectCacheImpl.module());
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfig.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfig.java
index fe25bb7..a004f6f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfig.java
@@ -141,6 +141,16 @@
     return RefPatternMatcher.getMatcher(refPattern).match(refName, null);
   }
 
+  // Project association
+  public Optional<String> getItsProjectName(Project.NameKey projectNK) {
+    ProjectState projectState = projectCache.get(projectNK);
+    if (projectState == null) {
+      return Optional.empty();
+    }
+    return Optional.ofNullable(
+        pluginCfgFactory.getFromProjectConfig(projectState, pluginName).getString("its-project"));
+  }
+
   // Issue association --------------------------------------------------------
 
   /**
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsFacade.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsFacade.java
index 4cd7bc2..db2857b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsFacade.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/ItsFacade.java
@@ -38,6 +38,10 @@
 
   public void performAction(String issueId, String actionName) throws IOException;
 
+  default void createVersion(String itsProject, String version) throws IOException {
+    throw new UnsupportedOperationException("create-version is not implemented by " + getClass());
+  }
+
   public boolean exists(final String issueId) throws IOException;
 
   public String createLinkForWebui(String url, String text);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/NoopItsFacade.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/NoopItsFacade.java
index 85d806e..ecf3a6f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/its/NoopItsFacade.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/its/NoopItsFacade.java
@@ -62,6 +62,13 @@
   }
 
   @Override
+  public void createVersion(String itsProject, String version) {
+    if (log.isDebugEnabled()) {
+      log.debug("createVersion({},{})", itsProject, version);
+    }
+  }
+
+  @Override
   public String healthCheck(Check check) throws IOException {
     if (log.isDebugEnabled()) {
       log.debug("healthCheck()");
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/util/ItsProjectExtractor.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/util/ItsProjectExtractor.java
new file mode 100644
index 0000000..e872f8d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/util/ItsProjectExtractor.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2018 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.its.base.util;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.its.base.its.ItsConfig;
+import java.util.Optional;
+
+public class ItsProjectExtractor {
+
+  private final ItsConfig itsConfig;
+
+  @Inject
+  ItsProjectExtractor(ItsConfig itsConfig) {
+    this.itsConfig = itsConfig;
+  }
+
+  public Optional<String> getItsProject(String gerritProjectName) {
+    return itsConfig.getItsProjectName(new Project.NameKey(gerritProjectName));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractor.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractor.java
index 5fde1ab..a59de25 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractor.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.events.RefEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.its.base.workflow.RefEventProperties;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -41,16 +42,19 @@
 
 /** Extractor to translate an {@link ChangeEvent} to a map of properties}. */
 public class PropertyExtractor {
-  private IssueExtractor issueExtractor;
-  private PropertyAttributeExtractor propertyAttributeExtractor;
+  private final ItsProjectExtractor itsProjectExtractor;
+  private final IssueExtractor issueExtractor;
+  private final PropertyAttributeExtractor propertyAttributeExtractor;
   private final String pluginName;
 
   @Inject
   PropertyExtractor(
       IssueExtractor issueExtractor,
+      ItsProjectExtractor itsProjectExtractor,
       PropertyAttributeExtractor propertyAttributeExtractor,
       @PluginName String pluginName) {
     this.issueExtractor = issueExtractor;
+    this.itsProjectExtractor = itsProjectExtractor;
     this.propertyAttributeExtractor = propertyAttributeExtractor;
     this.pluginName = pluginName;
   }
@@ -167,12 +171,17 @@
    * @param event The event to extract property maps from.
    * @return set of property maps extracted from the event.
    */
-  public Set<Map<String, String>> extractFrom(RefEvent event) {
+  public RefEventProperties extractFrom(RefEvent event) {
     Map<String, Set<String>> associations = null;
     Map<String, String> common = new HashMap<>();
     common.put("event", event.getClass().getName());
+    String project = event.getProjectNameKey().get();
     common.put("event-type", event.type);
-    common.put("project", event.getProjectNameKey().get());
+    common.put("project", project);
+
+    itsProjectExtractor
+        .getItsProject(project)
+        .ifPresent(itsProject -> common.put("its-project", itsProject));
     common.put("ref", event.getRefName());
     common.put("itsName", pluginName);
 
@@ -200,6 +209,6 @@
         ret.add(properties);
       }
     }
-    return ret;
+    return new RefEventProperties(common, ret);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/Action.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/Action.java
index 0f46ce6..338ad81 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/Action.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/Action.java
@@ -21,15 +21,18 @@
 /** Interface for actions on an issue tracking system */
 interface Action {
 
+  /** @return The type of this action */
+  ActionType getType();
+
   /**
    * Execute this action.
    *
    * @param its The facade interface to execute actions.
-   * @param issue The issue to execute on.
+   * @param target The target to execute on. Its kind will depend on the action type.
    * @param actionRequest The request to execute.
    * @param properties The properties for the execution.
    */
   void execute(
-      ItsFacade its, String issue, ActionRequest actionRequest, Map<String, String> properties)
+      ItsFacade its, String target, ActionRequest actionRequest, Map<String, String> properties)
       throws IOException;
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionController.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionController.java
index 3329610..4af8f1b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionController.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionController.java
@@ -23,6 +23,8 @@
 import java.util.Collection;
 import java.util.Map;
 import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Controller that takes actions according to {@code ChangeEvents@}.
@@ -31,6 +33,9 @@
  * issue's status).
  */
 public class ActionController implements EventListener {
+
+  private static final Logger log = LoggerFactory.getLogger(ActionController.class);
+
   private final PropertyExtractor propertyExtractor;
   private final RuleBase ruleBase;
   private final ActionExecutor actionExecutor;
@@ -59,12 +64,39 @@
   }
 
   private void handleEvent(RefEvent refEvent) {
-    Set<Map<String, String>> properties = propertyExtractor.extractFrom(refEvent);
-    for (Map<String, String> propertiesMap : properties) {
-      Collection<ActionRequest> actions = ruleBase.actionRequestsFor(propertiesMap);
+    RefEventProperties refEventProperties = propertyExtractor.extractFrom(refEvent);
+
+    handleIssuesEvent(refEventProperties.getIssuesProperties());
+    handleProjectEvent(refEventProperties.getProjectProperties());
+  }
+
+  private void handleIssuesEvent(Set<Map<String, String>> issuesProperties) {
+    for (Map<String, String> issueProperties : issuesProperties) {
+      Collection<ActionRequest> actions = ruleBase.actionRequestsFor(issueProperties);
       if (!actions.isEmpty()) {
-        actionExecutor.execute(actions, propertiesMap);
+        actionExecutor.executeOnIssue(actions, issueProperties);
       }
     }
   }
+
+  private void handleProjectEvent(Map<String, String> projectProperties) {
+    if (projectProperties.isEmpty()) {
+      return;
+    }
+
+    Collection<ActionRequest> projectActions = ruleBase.actionRequestsFor(projectProperties);
+    if (projectActions.isEmpty()) {
+      return;
+    }
+    if (!projectProperties.containsKey("its-project")) {
+      String project = projectProperties.get("project");
+      log.error(
+          "Could not process project event. No its-project associated with project {}. "
+              + "Did you forget to configure the ITS project association in project.config?",
+          project);
+      return;
+    }
+
+    actionExecutor.executeOnProject(projectActions, projectProperties);
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutor.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutor.java
index e209daf..bd42552 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutor.java
@@ -33,6 +33,7 @@
   private final AddSoyComment.Factory addSoyCommentFactory;
   private final LogEvent.Factory logEventFactory;
   private final AddPropertyToField.Factory addPropertyToFieldFactory;
+  private final CreateVersionFromProperty.Factory createVersionFromPropertyFactory;
 
   @Inject
   public ActionExecutor(
@@ -41,13 +42,15 @@
       AddStandardComment.Factory addStandardCommentFactory,
       AddSoyComment.Factory addSoyCommentFactory,
       LogEvent.Factory logEventFactory,
-      AddPropertyToField.Factory addPropertyToFieldFactory) {
+      AddPropertyToField.Factory addPropertyToFieldFactory,
+      CreateVersionFromProperty.Factory createVersionFromPropertyFactory) {
     this.itsFactory = itsFactory;
     this.addCommentFactory = addCommentFactory;
     this.addStandardCommentFactory = addStandardCommentFactory;
     this.addSoyCommentFactory = addSoyCommentFactory;
     this.logEventFactory = logEventFactory;
     this.addPropertyToFieldFactory = addPropertyToFieldFactory;
+    this.createVersionFromPropertyFactory = createVersionFromPropertyFactory;
   }
 
   private Action getAction(String actionName) {
@@ -62,18 +65,21 @@
         return logEventFactory.create();
       case "add-property-to-field":
         return addPropertyToFieldFactory.create();
+      case "create-version-from-property":
+        return createVersionFromPropertyFactory.create();
       default:
         return null;
     }
   }
 
-  private void execute(String issue, ActionRequest actionRequest, Map<String, String> properties) {
+  private void executeOnIssue(
+      String issue, ActionRequest actionRequest, Map<String, String> properties) {
     ItsFacade its = itsFactory.getFacade(new Project.NameKey(properties.get("project")));
     try {
       Action action = getAction(actionRequest.getName());
       if (action == null) {
         its.performAction(issue, actionRequest.getUnparsed());
-      } else {
+      } else if (action.getType() == ActionType.ISSUE) {
         action.execute(its, issue, actionRequest, properties);
       }
     } catch (IOException e) {
@@ -81,9 +87,34 @@
     }
   }
 
-  public void execute(Iterable<ActionRequest> actions, Map<String, String> properties) {
+  public void executeOnIssue(Iterable<ActionRequest> actions, Map<String, String> properties) {
     for (ActionRequest actionRequest : actions) {
-      execute(properties.get("issue"), actionRequest, properties);
+      executeOnIssue(properties.get("issue"), actionRequest, properties);
+    }
+  }
+
+  private void executeOnProject(
+      String itsProject, ActionRequest actionRequest, Map<String, String> properties) {
+    try {
+      String actionName = actionRequest.getName();
+      Action action = getAction(actionName);
+      if (action == null) {
+        log.debug("No action found for name {}", actionName);
+        return;
+      }
+      if (action.getType() != ActionType.PROJECT) {
+        return;
+      }
+      ItsFacade its = itsFactory.getFacade(new Project.NameKey(properties.get("project")));
+      action.execute(its, itsProject, actionRequest, properties);
+    } catch (IOException e) {
+      log.error("Error while executing action " + actionRequest, e);
+    }
+  }
+
+  public void executeOnProject(Iterable<ActionRequest> actions, Map<String, String> properties) {
+    for (ActionRequest actionRequest : actions) {
+      executeOnProject(properties.get("its-project"), actionRequest, properties);
     }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionType.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionType.java
new file mode 100644
index 0000000..8d18cce
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionType.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2018 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.its.base.workflow;
+
+public enum ActionType {
+  /** Actions that will be executed on ITS issues */
+  ISSUE,
+  /** Actions that will be executed on ITS projects */
+  PROJECT
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddComment.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddComment.java
index 82d1701..2624613 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddComment.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddComment.java
@@ -24,7 +24,7 @@
  *
  * <p>The action requests parameters get concatenated and get added to the issue.
  */
-public class AddComment implements Action {
+public class AddComment extends IssueAction {
   public interface Factory {
     AddComment create();
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToField.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToField.java
index 1979183..c19aa73 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToField.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddPropertyToField.java
@@ -20,7 +20,7 @@
 import java.util.Map;
 import java.util.Optional;
 
-public class AddPropertyToField implements Action {
+public class AddPropertyToField extends IssueAction {
 
   public interface Factory {
     AddPropertyToField create();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddSoyComment.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddSoyComment.java
index 035a6da..c29d622 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddSoyComment.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddSoyComment.java
@@ -39,7 +39,7 @@
  *
  * <p>Comments are added for merging, abandoning, restoring of changes and adding of patch sets.
  */
-public class AddSoyComment implements Action {
+public class AddSoyComment extends IssueAction {
   private static final Logger log = LoggerFactory.getLogger(AddSoyComment.class);
 
   public interface Factory {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddStandardComment.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddStandardComment.java
index c2b95a6..d289024 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddStandardComment.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/AddStandardComment.java
@@ -24,7 +24,7 @@
  *
  * <p>Comments are added for merging, abandoning, restoring of changes and adding of patch sets.
  */
-public class AddStandardComment implements Action {
+public class AddStandardComment extends IssueAction {
   public interface Factory {
     AddStandardComment create();
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/CreateVersionFromProperty.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/CreateVersionFromProperty.java
new file mode 100644
index 0000000..91cd34d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/CreateVersionFromProperty.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2018 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.its.base.workflow;
+
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Optional;
+
+/** Creates a version in the ITS. The value of the version is extracted from an event property. */
+public class CreateVersionFromProperty extends ProjectAction {
+
+  public interface Factory {
+    CreateVersionFromProperty create();
+  }
+
+  private final CreateVersionFromPropertyParametersExtractor parametersExtractor;
+
+  @Inject
+  public CreateVersionFromProperty(
+      CreateVersionFromPropertyParametersExtractor parametersExtractor) {
+    this.parametersExtractor = parametersExtractor;
+  }
+
+  @Override
+  public void execute(
+      ItsFacade its, String itsProject, ActionRequest actionRequest, Map<String, String> properties)
+      throws IOException {
+    Optional<CreateVersionFromPropertyParameters> parameters =
+        parametersExtractor.extract(actionRequest, properties);
+    if (!parameters.isPresent()) {
+      return;
+    }
+
+    its.createVersion(itsProject, parameters.get().getPropertyValue());
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/CreateVersionFromPropertyParameters.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/CreateVersionFromPropertyParameters.java
new file mode 100644
index 0000000..2ddc245
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/CreateVersionFromPropertyParameters.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2018 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.its.base.workflow;
+
+/** Parameters needed by {@link CreateVersionFromProperty} action */
+public class CreateVersionFromPropertyParameters {
+
+  private final String propertyValue;
+
+  public CreateVersionFromPropertyParameters(String propertyValue) {
+    this.propertyValue = propertyValue;
+  }
+
+  /** @return The extracted property value that will be used as the version value */
+  public String getPropertyValue() {
+    return propertyValue;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/CreateVersionFromPropertyParametersExtractor.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/CreateVersionFromPropertyParametersExtractor.java
new file mode 100644
index 0000000..1eb01ce
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/CreateVersionFromPropertyParametersExtractor.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2018 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.its.base.workflow;
+
+import com.google.common.base.Strings;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+import javax.inject.Inject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class CreateVersionFromPropertyParametersExtractor {
+
+  private static final Logger log =
+      LoggerFactory.getLogger(CreateVersionFromPropertyParametersExtractor.class);
+
+  @Inject
+  public CreateVersionFromPropertyParametersExtractor() {}
+
+  public Optional<CreateVersionFromPropertyParameters> extract(
+      ActionRequest actionRequest, Map<String, String> properties) {
+    String[] parameters = actionRequest.getParameters();
+    if (parameters.length != 1) {
+      log.error(
+          "Wrong number of received parameters. Received parameters are {}. Only one parameter is expected, the property id.",
+          Arrays.toString(parameters));
+      return Optional.empty();
+    }
+
+    String propertyId = parameters[0];
+    if (Strings.isNullOrEmpty(propertyId)) {
+      log.error("Received property id is blank");
+      return Optional.empty();
+    }
+
+    if (!properties.containsKey(propertyId)) {
+      log.error("No event property found for id {}", propertyId);
+      return Optional.empty();
+    }
+
+    String propertyValue = properties.get(propertyId);
+    return Optional.of(new CreateVersionFromPropertyParameters(propertyValue));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/IssueAction.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/IssueAction.java
new file mode 100644
index 0000000..230517f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/IssueAction.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2018 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.its.base.workflow;
+
+/** Abstraction for actions on ITS issues */
+public abstract class IssueAction implements Action {
+
+  @Override
+  public final ActionType getType() {
+    return ActionType.ISSUE;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/LogEvent.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/LogEvent.java
index 1f12f19..6eec9a4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/LogEvent.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/LogEvent.java
@@ -27,7 +27,7 @@
  *
  * <p>This event helps when developing rules as available properties become visible.
  */
-public class LogEvent implements Action {
+public class LogEvent extends IssueAction {
   private static final Logger log = LoggerFactory.getLogger(LogEvent.class);
 
   private enum Level {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ProjectAction.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ProjectAction.java
new file mode 100644
index 0000000..4c3c55b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ProjectAction.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2018 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.its.base.workflow;
+
+/** Abstraction for actions on ITS projects */
+public abstract class ProjectAction implements Action {
+
+  @Override
+  public final ActionType getType() {
+    return ActionType.PROJECT;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/RefEventProperties.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/RefEventProperties.java
new file mode 100644
index 0000000..30940bb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/RefEventProperties.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2018 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.its.base.workflow;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+/** The properties extracted from a {@link com.google.gerrit.server.events.RefEvent} */
+public class RefEventProperties {
+
+  private final Map<String, String> projectProperties;
+  private final Set<Map<String, String>> issuesProperties;
+
+  /**
+   * @param projectProperties Properties of the ref event
+   * @param issuesProperties Properties of the ref event added of the properties specific to the
+   *     issues. There will be as many set of properties as number of issues
+   */
+  public RefEventProperties(
+      Map<String, String> projectProperties, Set<Map<String, String>> issuesProperties) {
+    this.projectProperties = Collections.unmodifiableMap(projectProperties);
+    this.issuesProperties = Collections.unmodifiableSet(issuesProperties);
+  }
+
+  /** @return Properties of the ref event */
+  public Map<String, String> getProjectProperties() {
+    return projectProperties;
+  }
+
+  /**
+   * @return Properties of the ref event added of the properties specific to the issues. There will
+   *     be as many set of properties as number of issues
+   */
+  public Set<Map<String, String>> getIssuesProperties() {
+    return issuesProperties;
+  }
+}
diff --git a/src/main/resources/Documentation/config-common.md b/src/main/resources/Documentation/config-common.md
index 9532510..72d5e24 100644
--- a/src/main/resources/Documentation/config-common.md
+++ b/src/main/resources/Documentation/config-common.md
@@ -122,6 +122,23 @@
     branch = ^refs/heads/stable-.*
 ```
 
+[associating-its-project]: #associating-its-project
+<a name="associating-its-project">Associating a Gerrit project with its ITS
+project counterpart</a>
+---------------------------------------------------------------
+
+To be able to make use of actions acting at the ITS project level, you must
+associate a Gerrit project to its ITS project counterpart.
+
+It must be configured per project and per plugin. To configure the association
+for a project mapping to an ITS project named `manhattan-project`, the project
+must have the following entry in its `project.config` file in the
+`refs/meta/config` branch:
+
+```
+  [plugin "@PLUGIN@"]
+    its-project = manhattan-project
+```
 
 
 [configure-rules]: #configure-rules
diff --git a/src/main/resources/Documentation/config-rulebase-common.md b/src/main/resources/Documentation/config-rulebase-common.md
index 65c18b1..64059bf 100644
--- a/src/main/resources/Documentation/config-rulebase-common.md
+++ b/src/main/resources/Documentation/config-rulebase-common.md
@@ -564,6 +564,9 @@
 [`add-soy-comment`][action-add-soy-comment]
 : adds a rendered Closure Template (soy) template as issue comment
 
+[`create-version-from-property`][action-create-version-from-property]
+: creates a version based on an event's property value
+
 [`log-event`][action-log-event]
 : appends the event's properties to Gerrit's log
 
@@ -648,6 +651,22 @@
   action = add-property-to-field branch labels
 ```
 
+[action-create-version-from-property]: #create-version-from-property
+### <a name="create-version-from-property">Action: create-version-from-property</a>
+
+The `create-version-from-property` action creates a version in the ITS project
+by using an event property value as the version value.
+
+This is useful when you want to create a new version in the ITS when a tag is
+created in the Gerrit project.
+
+Example with the event property `ref`:
+
+```
+  action = create-version-from-property ref
+```
+
+
 [action-log-event]: #action-log-event
 ### <a name="action-log-event">Action: log-event</a>
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfigTest.java
index 9664542..1a147c7 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfigTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/its/ItsConfigTest.java
@@ -14,6 +14,7 @@
 
 package com.googlesource.gerrit.plugins.its.base.its;
 
+import static org.easymock.EasyMock.eq;
 import static org.easymock.EasyMock.expect;
 
 import com.google.common.base.Suppliers;
@@ -38,6 +39,7 @@
 import com.googlesource.gerrit.plugins.its.base.testutil.LoggingMockingTestCase;
 import com.googlesource.gerrit.plugins.its.base.validation.ItsAssociationPolicy;
 import java.util.Arrays;
+import java.util.Optional;
 import org.eclipse.jgit.lib.Config;
 
 public class ItsConfigTest extends LoggingMockingTestCase {
@@ -47,7 +49,8 @@
   private PluginConfigFactory pluginConfigFactory;
   private Config serverConfig;
 
-  public void setupIsEnabled(String enabled, String parentEnabled, String[] branches) {
+  public void setupIsEnabled(
+      String enabled, String itsProject, String parentEnabled, String[] branches) {
     ProjectState projectState = createMock(ProjectState.class);
 
     expect(projectCache.get(new Project.NameKey("testProject"))).andReturn(projectState).anyTimes();
@@ -81,7 +84,7 @@
 
       parents = Arrays.asList(parentProjectState, projectState);
     }
-    expect(projectState.treeInOrder()).andReturn(parents);
+    expect(projectState.treeInOrder()).andReturn(parents).anyTimes();
 
     PluginConfig pluginConfig = createMock(PluginConfig.class);
 
@@ -90,6 +93,7 @@
         .anyTimes();
 
     expect(pluginConfig.getString("enabled", "false")).andReturn(enabled).anyTimes();
+    expect(pluginConfig.getString(eq("its-project"))).andReturn(itsProject).anyTimes();
 
     PluginConfig pluginConfigWI = createMock(PluginConfig.class);
 
@@ -104,7 +108,7 @@
 
   public void testIsEnabledRefNoParentNoBranchEnabled() {
     String[] branches = {};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     ItsConfig itsConfig = createItsConfig();
 
@@ -116,7 +120,7 @@
 
   public void BROKEN_testIsEnabledRefNoParentNoBranchDisabled() {
     String[] branches = {};
-    setupIsEnabled("false", null, branches);
+    setupIsEnabled("false", null, null, branches);
 
     ItsConfig itsConfig = createItsConfig();
 
@@ -128,7 +132,7 @@
 
   public void testIsEnabledRefNoParentNoBranchEnforced() {
     String[] branches = {};
-    setupIsEnabled("enforced", null, branches);
+    setupIsEnabled("enforced", null, null, branches);
 
     ItsConfig itsConfig = createItsConfig();
 
@@ -140,7 +144,7 @@
 
   public void testIsEnabledRefNoParentMatchingBranchEnabled() {
     String[] branches = {"^refs/heads/test.*"};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     ItsConfig itsConfig = createItsConfig();
 
@@ -152,7 +156,7 @@
 
   public void BROKEN_testIsEnabledRefNoParentMatchingBranchDisabled() {
     String[] branches = {"^refs/heads/test.*"};
-    setupIsEnabled("false", null, branches);
+    setupIsEnabled("false", null, null, branches);
 
     ItsConfig itsConfig = createItsConfig();
 
@@ -164,7 +168,7 @@
 
   public void testIsEnabledRefNoParentMatchingBranchEnforced() {
     String[] branches = {"^refs/heads/test.*"};
-    setupIsEnabled("enforced", null, branches);
+    setupIsEnabled("enforced", null, null, branches);
 
     ItsConfig itsConfig = createItsConfig();
 
@@ -176,7 +180,7 @@
 
   public void BROKEN_testIsEnabledRefNoParentNonMatchingBranchEnabled() {
     String[] branches = {"^refs/heads/foo.*"};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     ItsConfig itsConfig = createItsConfig();
 
@@ -188,7 +192,7 @@
 
   public void BROKEN_testIsEnabledRefNoParentNonMatchingBranchDisabled() {
     String[] branches = {"^refs/heads/foo.*"};
-    setupIsEnabled("false", null, branches);
+    setupIsEnabled("false", null, null, branches);
 
     ItsConfig itsConfig = createItsConfig();
 
@@ -200,7 +204,7 @@
 
   public void BROKEN_testIsEnabledRefNoParentNonMatchingBranchEnforced() {
     String[] branches = {"^refs/heads/foo.*"};
-    setupIsEnabled("enforced", null, branches);
+    setupIsEnabled("enforced", null, null, branches);
 
     ItsConfig itsConfig = createItsConfig();
 
@@ -212,7 +216,7 @@
 
   public void testIsEnabledRefNoParentMatchingBranchMiddleEnabled() {
     String[] branches = {"^refs/heads/foo.*", "^refs/heads/test.*", "^refs/heads/baz.*"};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     ItsConfig itsConfig = createItsConfig();
 
@@ -224,7 +228,7 @@
 
   public void BROKEN_testIsEnabledRefNoParentMatchingBranchMiddleDisabled() {
     String[] branches = {"^refs/heads/foo.*", "^refs/heads/test.*", "^refs/heads/baz.*"};
-    setupIsEnabled("false", null, branches);
+    setupIsEnabled("false", null, null, branches);
 
     ItsConfig itsConfig = createItsConfig();
 
@@ -236,7 +240,7 @@
 
   public void testIsEnabledRefNoParentMatchingBranchMiddleEnforced() {
     String[] branches = {"^refs/heads/foo.*", "^refs/heads/test.*", "^refs/heads/baz.*"};
-    setupIsEnabled("enforced", null, branches);
+    setupIsEnabled("enforced", null, null, branches);
 
     ItsConfig itsConfig = createItsConfig();
 
@@ -248,7 +252,7 @@
 
   public void BROKEN_testIsEnabledRefParentNoBranchEnabled() {
     String[] branches = {};
-    setupIsEnabled("false", "true", branches);
+    setupIsEnabled("false", null, "true", branches);
 
     ItsConfig itsConfig = createItsConfig();
 
@@ -260,7 +264,7 @@
 
   public void BROKEN_testIsEnabledRefParentNoBranchDisabled() {
     String[] branches = {};
-    setupIsEnabled("false", "false", branches);
+    setupIsEnabled("false", null, "false", branches);
 
     ItsConfig itsConfig = createItsConfig();
 
@@ -272,7 +276,7 @@
 
   public void testIsEnabledRefParentNoBranchEnforced() {
     String[] branches = {};
-    setupIsEnabled("false", "enforced", branches);
+    setupIsEnabled("false", null, "enforced", branches);
 
     ItsConfig itsConfig = createItsConfig();
 
@@ -284,7 +288,7 @@
 
   public void testIsEnabledEventNoBranches() {
     String[] branches = {};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     PatchSetCreatedEvent event = new PatchSetCreatedEvent(testChange("testProject", "testBranch"));
 
@@ -297,7 +301,7 @@
 
   public void testIsEnabledEventSingleBranchExact() {
     String[] branches = {"refs/heads/testBranch"};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     PatchSetCreatedEvent event = new PatchSetCreatedEvent(testChange("testProject", "testBranch"));
 
@@ -310,7 +314,7 @@
 
   public void testIsEnabledEventSingleBranchRegExp() {
     String[] branches = {"^refs/heads/test.*"};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     PatchSetCreatedEvent event = new PatchSetCreatedEvent(testChange("testProject", "testBranch"));
 
@@ -323,7 +327,7 @@
 
   public void BROKEN_testIsEnabledEventSingleBranchNonMatchingRegExp() {
     String[] branches = {"^refs/heads/foo.*"};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     PatchSetCreatedEvent event = new PatchSetCreatedEvent(testChange("testProject", "testBranch"));
 
@@ -336,7 +340,7 @@
 
   public void testIsEnabledEventMultiBranchExact() {
     String[] branches = {"refs/heads/foo", "refs/heads/testBranch"};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     PatchSetCreatedEvent event = new PatchSetCreatedEvent(testChange("testProject", "testBranch"));
 
@@ -349,7 +353,7 @@
 
   public void testIsEnabledEventMultiBranchRegExp() {
     String[] branches = {"^refs/heads/foo.*", "^refs/heads/test.*"};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     PatchSetCreatedEvent event = new PatchSetCreatedEvent(testChange("testProject", "testBranch"));
 
@@ -362,7 +366,7 @@
 
   public void testIsEnabledEventMultiBranchMixedMatchExact() {
     String[] branches = {"refs/heads/testBranch", "refs/heads/foo.*"};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     PatchSetCreatedEvent event = new PatchSetCreatedEvent(testChange("testProject", "testBranch"));
 
@@ -375,7 +379,7 @@
 
   public void testIsEnabledEventMultiBranchMixedMatchRegExp() {
     String[] branches = {"refs/heads/foo", "^refs/heads/test.*"};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     PatchSetCreatedEvent event = new PatchSetCreatedEvent(testChange("testProject", "testBranch"));
 
@@ -388,7 +392,7 @@
 
   public void BROKEN_testIsEnabledEventDisabled() {
     String[] branches = {"^refs/heads/testBranch"};
-    setupIsEnabled("false", null, branches);
+    setupIsEnabled("false", null, null, branches);
 
     PatchSetCreatedEvent event = new PatchSetCreatedEvent(testChange("testProject", "testBranch"));
 
@@ -401,7 +405,7 @@
 
   public void testIsEnabledCommentAddedEvent() {
     String[] branches = {};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     CommentAddedEvent event = new CommentAddedEvent(testChange("testProject", "testBranch"));
 
@@ -414,7 +418,7 @@
 
   public void testIsEnabledChangeMergedEvent() {
     String[] branches = {};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     ChangeMergedEvent event = new ChangeMergedEvent(testChange("testProject", "testBranch"));
 
@@ -427,7 +431,7 @@
 
   public void testIsEnabledChangeAbandonedEvent() {
     String[] branches = {};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     ChangeAbandonedEvent event = new ChangeAbandonedEvent(testChange("testProject", "testBranch"));
 
@@ -440,7 +444,7 @@
 
   public void testIsEnabledChangeRestoredEvent() {
     String[] branches = {};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     ChangeRestoredEvent event = new ChangeRestoredEvent(testChange("testProject", "testBranch"));
 
@@ -453,7 +457,7 @@
 
   public void testIsEnabledRefUpdatedEvent() {
     String[] branches = {};
-    setupIsEnabled("true", null, branches);
+    setupIsEnabled("true", null, null, branches);
 
     RefUpdatedEvent event = new RefUpdatedEvent();
     RefUpdateAttribute refUpdateAttribute = new RefUpdateAttribute();
@@ -479,6 +483,31 @@
     assertLogMessageContains("not recognised and ignored");
   }
 
+  public void testGetItsProjectNull() {
+    String[] branches = {};
+    setupIsEnabled("true", null, null, branches);
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertFalse(itsConfig.getItsProjectName(new Project.NameKey("testProject")).isPresent());
+  }
+
+  public void testGetItsProjectConfigured() {
+    String[] branches = {};
+    setupIsEnabled("true", "itsProject", null, branches);
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    Optional<String> itsProjectName =
+        itsConfig.getItsProjectName(new Project.NameKey("testProject"));
+    assertTrue(itsProjectName.isPresent());
+    assertEquals("itsProject", itsProjectName.get());
+  }
+
   public void testGetIssuePatternNullMatch() {
     ItsConfig itsConfig = createItsConfig();
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/util/ItsProjectExtractorTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/util/ItsProjectExtractorTest.java
new file mode 100644
index 0000000..0aecd05
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/util/ItsProjectExtractorTest.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2018 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.its.base.util;
+
+import static org.easymock.EasyMock.expect;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.its.base.its.ItsConfig;
+import com.googlesource.gerrit.plugins.its.base.testutil.MockingTestCase;
+import java.util.Optional;
+
+public class ItsProjectExtractorTest extends MockingTestCase {
+
+  private static final String PROJECT = "project";
+  private static final String ITS_PROJECT = "itsProject";
+
+  private Injector injector;
+  private ItsConfig itsConfig;
+
+  public void test() {
+    ItsProjectExtractor projectExtractor = injector.getInstance(ItsProjectExtractor.class);
+
+    expect(itsConfig.getItsProjectName(new Project.NameKey(PROJECT)))
+        .andReturn(Optional.of(ITS_PROJECT))
+        .once();
+
+    replayMocks();
+
+    String ret = projectExtractor.getItsProject(PROJECT).orElse(null);
+    assertEquals(ret, ITS_PROJECT);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+
+    injector = Guice.createInjector(new TestModule());
+  }
+
+  private class TestModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      itsConfig = createMock(ItsConfig.class);
+      bind(ItsConfig.class).toInstance(itsConfig);
+    }
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractorTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractorTest.java
index 249abcd..d2a7480 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/util/PropertyExtractorTest.java
@@ -43,20 +43,25 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 
 public class PropertyExtractorTest extends LoggingMockingTestCase {
   private Injector injector;
 
+  private ItsProjectExtractor itsProjectExtractor;
   private IssueExtractor issueExtractor;
   private PropertyAttributeExtractor propertyAttributeExtractor;
 
   public void testDummyChangeEvent() {
     PropertyExtractor propertyExtractor = injector.getInstance(PropertyExtractor.class);
 
+    expect(itsProjectExtractor.getItsProject("testProject")).andReturn(Optional.empty());
+
     replayMocks();
 
-    Set<Map<String, String>> actual = propertyExtractor.extractFrom(new DummyEvent());
+    Set<Map<String, String>> actual =
+        propertyExtractor.extractFrom(new DummyEvent()).getIssuesProperties();
 
     Set<Map<String, String>> expected = new HashSet<>();
     assertEquals("Properties do not match", expected, actual);
@@ -65,6 +70,8 @@
   public void testChangeAbandonedEvent() {
     ChangeAbandonedEvent event = new ChangeAbandonedEvent(testChange("testProject", "testBranch"));
 
+    expect(itsProjectExtractor.getItsProject("testProject")).andReturn(Optional.empty());
+
     ChangeAttribute changeAttribute = createMock(ChangeAttribute.class);
     event.change = Suppliers.ofInstance(changeAttribute);
     Map<String, String> changeProperties =
@@ -104,6 +111,8 @@
   public void testChangeMergedEvent() {
     ChangeMergedEvent event = new ChangeMergedEvent(testChange("testProject", "testBranch"));
 
+    expect(itsProjectExtractor.getItsProject("testProject")).andReturn(Optional.empty());
+
     ChangeAttribute changeAttribute = createMock(ChangeAttribute.class);
     event.change = Suppliers.ofInstance(changeAttribute);
     Map<String, String> changeProperties =
@@ -141,6 +150,8 @@
   public void testChangeRestoredEvent() {
     ChangeRestoredEvent event = new ChangeRestoredEvent(testChange("testProject", "testBranch"));
 
+    expect(itsProjectExtractor.getItsProject("testProject")).andReturn(Optional.empty());
+
     ChangeAttribute changeAttribute = createMock(ChangeAttribute.class);
     event.change = Suppliers.ofInstance(changeAttribute);
     Map<String, String> changeProperties =
@@ -179,6 +190,8 @@
   public void testCommentAddedEventWOApprovals() {
     CommentAddedEvent event = new CommentAddedEvent(testChange("testProject", "testBranch"));
 
+    expect(itsProjectExtractor.getItsProject("testProject")).andReturn(Optional.empty());
+
     ChangeAttribute changeAttribute = createMock(ChangeAttribute.class);
     event.change = Suppliers.ofInstance(changeAttribute);
     Map<String, String> changeProperties =
@@ -218,6 +231,8 @@
   public void testCommentAddedEventWApprovals() {
     CommentAddedEvent event = new CommentAddedEvent(testChange("testProject", "testBranch"));
 
+    expect(itsProjectExtractor.getItsProject("testProject")).andReturn(Optional.empty());
+
     ChangeAttribute changeAttribute = createMock(ChangeAttribute.class);
     event.change = Suppliers.ofInstance(changeAttribute);
     Map<String, String> changeProperties =
@@ -270,6 +285,8 @@
   public void testPatchSetCreatedEvent() {
     PatchSetCreatedEvent event = new PatchSetCreatedEvent(testChange("testProject", "testBranch"));
 
+    expect(itsProjectExtractor.getItsProject("testProject")).andReturn(Optional.empty());
+
     ChangeAttribute changeAttribute = createMock(ChangeAttribute.class);
     event.change = Suppliers.ofInstance(changeAttribute);
     Map<String, String> changeProperties =
@@ -325,6 +342,8 @@
     refUpdateAttribute.oldRev = "oldRevision";
     refUpdateAttribute.refName = "testBranch";
 
+    expect(itsProjectExtractor.getItsProject("testProject")).andReturn(Optional.empty());
+
     Map<String, String> common =
         ImmutableMap.<String, String>builder()
             .putAll(accountProperties)
@@ -357,7 +376,7 @@
 
     replayMocks();
 
-    Set<Map<String, String>> actual = propertyExtractor.extractFrom(event);
+    Set<Map<String, String>> actual = propertyExtractor.extractFrom(event).getIssuesProperties();
 
     Map<String, String> propertiesIssue4711 =
         ImmutableMap.<String, String>builder()
@@ -395,6 +414,9 @@
     protected void configure() {
       bind(String.class).annotatedWith(PluginName.class).toInstance("ItsTestName");
 
+      itsProjectExtractor = createMock(ItsProjectExtractor.class);
+      bind(ItsProjectExtractor.class).toInstance(itsProjectExtractor);
+
       issueExtractor = createMock(IssueExtractor.class);
       bind(IssueExtractor.class).toInstance(issueExtractor);
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionControllerTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionControllerTest.java
index 8c8a7de..bd32089 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionControllerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionControllerTest.java
@@ -47,7 +47,9 @@
     ChangeEvent event = createMock(ChangeEvent.class);
 
     Set<Map<String, String>> propertySets = new HashSet<>();
-    expect(propertyExtractor.extractFrom(event)).andReturn(propertySets).anyTimes();
+    expect(propertyExtractor.extractFrom(event))
+        .andReturn(new RefEventProperties(Collections.emptyMap(), propertySets))
+        .anyTimes();
 
     replayMocks();
 
@@ -63,34 +65,49 @@
     Map<String, String> properties = ImmutableMap.of("fake", "property");
     propertySets.add(properties);
 
-    expect(propertyExtractor.extractFrom(event)).andReturn(propertySets).anyTimes();
+    expect(propertyExtractor.extractFrom(event))
+        .andReturn(new RefEventProperties(properties, propertySets))
+        .anyTimes();
 
     // When no issues are found in the commit message, the list of actions is empty
     // as there are no matchs with an empty map of properties.
     Collection<ActionRequest> actions = Collections.emptySet();
-    expect(ruleBase.actionRequestsFor(properties)).andReturn(actions).once();
+    expect(ruleBase.actionRequestsFor(properties)).andReturn(actions).times(2);
 
     replayMocks();
 
     actionController.onEvent(event);
   }
 
-  public void testSinglePropertyMapSingleActionSingleIssue() {
+  public void testSinglePropertyMapSingleIssueActionSingleProjectAction() {
     ActionController actionController = createActionController();
 
     ChangeEvent event = createMock(ChangeEvent.class);
 
-    Map<String, String> properties = ImmutableMap.of("issue", "testIssue");
+    Map<String, String> projectProperties = ImmutableMap.of("its-project", "itsProject");
 
-    Set<Map<String, String>> propertySets = ImmutableSet.of(properties);
+    Map<String, String> issueProperties =
+        ImmutableMap.<String, String>builder()
+            .putAll(projectProperties)
+            .put("issue", "testIssue")
+            .build();
 
-    expect(propertyExtractor.extractFrom(event)).andReturn(propertySets).anyTimes();
+    Set<Map<String, String>> propertySets = ImmutableSet.of(issueProperties);
 
-    ActionRequest actionRequest1 = createMock(ActionRequest.class);
-    Collection<ActionRequest> actionRequests = ImmutableList.of(actionRequest1);
-    expect(ruleBase.actionRequestsFor(properties)).andReturn(actionRequests).once();
+    expect(propertyExtractor.extractFrom(event))
+        .andReturn(new RefEventProperties(projectProperties, propertySets))
+        .anyTimes();
 
-    actionExecutor.execute(actionRequests, properties);
+    ActionRequest issueActionRequest1 = createMock(ActionRequest.class);
+    Collection<ActionRequest> issueActionRequests = ImmutableList.of(issueActionRequest1);
+    expect(ruleBase.actionRequestsFor(issueProperties)).andReturn(issueActionRequests).once();
+
+    ActionRequest projectActionRequest1 = createMock(ActionRequest.class);
+    Collection<ActionRequest> projectActionRequests = ImmutableList.of(projectActionRequest1);
+    expect(ruleBase.actionRequestsFor(projectProperties)).andReturn(projectActionRequests).once();
+
+    actionExecutor.executeOnIssue(issueActionRequests, issueProperties);
+    actionExecutor.executeOnProject(projectActionRequests, projectProperties);
 
     replayMocks();
 
@@ -107,7 +124,9 @@
 
     Set<Map<String, String>> propertySets = ImmutableSet.of(properties1, properties2);
 
-    expect(propertyExtractor.extractFrom(event)).andReturn(propertySets).anyTimes();
+    expect(propertyExtractor.extractFrom(event))
+        .andReturn(new RefEventProperties(Collections.emptyMap(), propertySets))
+        .anyTimes();
 
     ActionRequest actionRequest1 = createMock(ActionRequest.class);
     Collection<ActionRequest> actionRequests1 = ImmutableList.of(actionRequest1);
@@ -119,8 +138,8 @@
     expect(ruleBase.actionRequestsFor(properties1)).andReturn(actionRequests1).once();
     expect(ruleBase.actionRequestsFor(properties2)).andReturn(actionRequests2).once();
 
-    actionExecutor.execute(actionRequests1, properties1);
-    actionExecutor.execute(actionRequests2, properties2);
+    actionExecutor.executeOnIssue(actionRequests1, properties1);
+    actionExecutor.executeOnIssue(actionRequests2, properties2);
 
     replayMocks();
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutorTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutorTest.java
index 3864cdf..88cfd1a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ActionExecutorTest.java
@@ -26,6 +26,7 @@
 import com.googlesource.gerrit.plugins.its.base.its.ItsFacadeFactory;
 import com.googlesource.gerrit.plugins.its.base.testutil.LoggingMockingTestCase;
 import java.io.IOException;
+import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
 
@@ -39,9 +40,15 @@
   private AddSoyComment.Factory addSoyCommentFactory;
   private LogEvent.Factory logEventFactory;
   private AddPropertyToField.Factory addPropertyToFieldFactory;
+  private CreateVersionFromProperty.Factory createVersionFromPropertyFactory;
 
   private Map<String, String> properties =
       ImmutableMap.of("issue", "4711", "project", "testProject");
+  private Map<String, String> projectProperties =
+      ImmutableMap.<String, String>builder()
+          .putAll(properties)
+          .put("its-project", "itsTestProject")
+          .build();
 
   public void testExecuteItem() throws IOException {
     ActionRequest actionRequest = createMock(ActionRequest.class);
@@ -57,7 +64,7 @@
     replayMocks();
 
     ActionExecutor actionExecutor = createActionExecutor();
-    actionExecutor.execute(actionRequests, properties);
+    actionExecutor.executeOnIssue(actionRequests, properties);
   }
 
   public void testExecuteItemException() throws IOException {
@@ -75,7 +82,7 @@
     replayMocks();
 
     ActionExecutor actionExecutor = createActionExecutor();
-    actionExecutor.execute(actionRequests, properties);
+    actionExecutor.executeOnIssue(actionRequests, properties);
 
     assertLogThrowableMessageContains("injected exception 1");
   }
@@ -100,7 +107,7 @@
     replayMocks();
 
     ActionExecutor actionExecutor = createActionExecutor();
-    actionExecutor.execute(actionRequests, properties);
+    actionExecutor.executeOnIssue(actionRequests, properties);
   }
 
   public void testExecuteIterableExceptions() throws IOException {
@@ -131,7 +138,7 @@
     replayMocks();
 
     ActionExecutor actionExecutor = createActionExecutor();
-    actionExecutor.execute(actionRequests, properties);
+    actionExecutor.executeOnIssue(actionRequests, properties);
 
     assertLogThrowableMessageContains("injected exception 1");
     assertLogThrowableMessageContains("injected exception 3");
@@ -153,7 +160,7 @@
     replayMocks();
 
     ActionExecutor actionExecutor = createActionExecutor();
-    actionExecutor.execute(actionRequests, properties);
+    actionExecutor.executeOnIssue(actionRequests, properties);
   }
 
   public void testAddSoyCommentDelegation() throws IOException {
@@ -172,7 +179,7 @@
     replayMocks();
 
     ActionExecutor actionExecutor = createActionExecutor();
-    actionExecutor.execute(actionRequests, properties);
+    actionExecutor.executeOnIssue(actionRequests, properties);
   }
 
   public void testAddStandardCommentDelegation() throws IOException {
@@ -191,7 +198,7 @@
     replayMocks();
 
     ActionExecutor actionExecutor = createActionExecutor();
-    actionExecutor.execute(actionRequests, properties);
+    actionExecutor.executeOnIssue(actionRequests, properties);
   }
 
   public void testLogEventDelegation() throws IOException {
@@ -210,7 +217,25 @@
     replayMocks();
 
     ActionExecutor actionExecutor = createActionExecutor();
-    actionExecutor.execute(actionRequests, properties);
+    actionExecutor.executeOnIssue(actionRequests, properties);
+  }
+
+  public void testCreateVersionFromPropertyDelegation() throws IOException {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getName()).andReturn("create-version-from-property");
+
+    CreateVersionFromProperty createVersionFromProperty =
+        createMock(CreateVersionFromProperty.class);
+    expect(createVersionFromPropertyFactory.create()).andReturn(createVersionFromProperty);
+    expect(itsFacadeFactory.getFacade(new Project.NameKey(properties.get("project"))))
+        .andReturn(its);
+
+    createVersionFromProperty.execute(its, "itsTestProject", actionRequest, projectProperties);
+
+    replayMocks();
+
+    ActionExecutor actionExecutor = createActionExecutor();
+    actionExecutor.executeOnProject(Collections.singleton(actionRequest), projectProperties);
   }
 
   public void testAddPropertyToFieldDelegation() throws IOException {
@@ -229,7 +254,7 @@
     replayMocks();
 
     ActionExecutor actionExecutor = createActionExecutor();
-    actionExecutor.execute(actionRequests, properties);
+    actionExecutor.executeOnIssue(actionRequests, properties);
   }
 
   private ActionExecutor createActionExecutor() {
@@ -265,6 +290,9 @@
 
       addPropertyToFieldFactory = createMock(AddPropertyToField.Factory.class);
       bind(AddPropertyToField.Factory.class).toInstance(addPropertyToFieldFactory);
+
+      createVersionFromPropertyFactory = createMock(CreateVersionFromProperty.Factory.class);
+      bind(CreateVersionFromProperty.Factory.class).toInstance(createVersionFromPropertyFactory);
     }
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/CreateVersionFromPropertyParametersExtractorTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/CreateVersionFromPropertyParametersExtractorTest.java
new file mode 100644
index 0000000..82b33e7
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/CreateVersionFromPropertyParametersExtractorTest.java
@@ -0,0 +1,83 @@
+package com.googlesource.gerrit.plugins.its.base.workflow;
+
+import static org.easymock.EasyMock.expect;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.its.base.testutil.MockingTestCase;
+import java.util.Collections;
+import java.util.Optional;
+
+public class CreateVersionFromPropertyParametersExtractorTest extends MockingTestCase {
+
+  private static final String ITS_PROJECT = "test-project";
+  private static final String PROPERTY_ID = "propertyId";
+  private static final String PROPERTY_VALUE = "propertyValue";
+
+  private CreateVersionFromPropertyParametersExtractor extractor;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    Injector injector = Guice.createInjector(new TestModule());
+    extractor = injector.getInstance(CreateVersionFromPropertyParametersExtractor.class);
+  }
+
+  private class TestModule extends FactoryModule {}
+
+  public void testNoParameter() {
+    testWrongNumberOfReceivedParameters(new String[] {});
+  }
+
+  public void testTwoParameters() {
+    testWrongNumberOfReceivedParameters(new String[] {PROPERTY_ID, PROPERTY_ID});
+  }
+
+  private void testWrongNumberOfReceivedParameters(String[] parameters) {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getParameters()).andReturn(parameters);
+
+    replayMocks();
+
+    Optional<CreateVersionFromPropertyParameters> extractedParameters =
+        extractor.extract(actionRequest, Collections.emptyMap());
+    assertFalse(extractedParameters.isPresent());
+  }
+
+  public void testBlankPropertyId() {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getParameters()).andReturn(new String[] {""});
+
+    replayMocks();
+
+    Optional<CreateVersionFromPropertyParameters> extractedParameters =
+        extractor.extract(actionRequest, Collections.emptyMap());
+    assertFalse(extractedParameters.isPresent());
+  }
+
+  public void testUnknownPropertyId() {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getParameters()).andReturn(new String[] {PROPERTY_ID});
+
+    replayMocks();
+
+    Optional<CreateVersionFromPropertyParameters> extractedParameters =
+        extractor.extract(actionRequest, Collections.emptyMap());
+    assertFalse(extractedParameters.isPresent());
+  }
+
+  public void testHappyPath() {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getParameters()).andReturn(new String[] {PROPERTY_ID});
+
+    replayMocks();
+
+    Optional<CreateVersionFromPropertyParameters> extractedParameters =
+        extractor.extract(actionRequest, Collections.singletonMap(PROPERTY_ID, PROPERTY_VALUE));
+    if (!extractedParameters.isPresent()) {
+      fail();
+    }
+    assertEquals(PROPERTY_VALUE, extractedParameters.get().getPropertyValue());
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/CreateVersionFromPropertyTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/CreateVersionFromPropertyTest.java
new file mode 100644
index 0000000..a89509c
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/CreateVersionFromPropertyTest.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2018 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.its.base.workflow;
+
+import static org.easymock.EasyMock.expect;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.its.base.its.ItsFacade;
+import com.googlesource.gerrit.plugins.its.base.testutil.MockingTestCase;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import org.easymock.EasyMock;
+
+public class CreateVersionFromPropertyTest extends MockingTestCase {
+
+  private static final String ITS_PROJECT = "test-project";
+  private static final String PROPERTY_ID = "propertyId";
+  private static final String PROPERTY_VALUE = "propertyValue";
+
+  private Injector injector;
+  private ItsFacade its;
+  private CreateVersionFromPropertyParametersExtractor parametersExtractor;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    injector = Guice.createInjector(new TestModule());
+  }
+
+  private class TestModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      its = createMock(ItsFacade.class);
+      bind(ItsFacade.class).toInstance(its);
+
+      parametersExtractor = createMock(CreateVersionFromPropertyParametersExtractor.class);
+      bind(CreateVersionFromPropertyParametersExtractor.class).toInstance(parametersExtractor);
+    }
+  }
+
+  public void testHappyPath() throws IOException {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+
+    Map<String, String> properties = Collections.emptyMap();
+    expect(parametersExtractor.extract(actionRequest, properties))
+        .andReturn(Optional.of(new CreateVersionFromPropertyParameters(PROPERTY_VALUE)));
+
+    its.createVersion(ITS_PROJECT, PROPERTY_VALUE);
+    EasyMock.expectLastCall().once();
+
+    replayMocks();
+
+    CreateVersionFromProperty createVersionFromProperty = createCreateVersionFromProperty();
+    createVersionFromProperty.execute(its, ITS_PROJECT, actionRequest, properties);
+  }
+
+  private CreateVersionFromProperty createCreateVersionFromProperty() {
+    return injector.getInstance(CreateVersionFromProperty.class);
+  }
+}