Repository: vackosar/gitflow-incremental-builder
Pull request #89 (2019-01-28 00:13:45)

Issue #44:  Introduce buildUpstreamMode & buildDownstream
Introduce buildUpstreamMode

- see README.md for the intended behaviour
- re-organize properties more logically
- remove unused repositorySshKey property

Introduce buildDownstream


Patch:
diff --git a/README.md b/README.md
index 42026bd..fd52616 100644
--- a/README.md
+++ b/README.md
@@ -22,9 +22,14 @@ This extension is **not limited to Git Flow setups!** The [extensive configurati
   - [gib.disableBranchComparison](#gibdisablebranchcomparison)
   - [gib.uncommited](#gibuncommited)
   - [gib.untracked](#gibuntracked)
-  - [gib.skipTestsForNotImpactedModules](#gibskiptestsfornotimpactedmodules)
-  - [gib.argsForNotImpactedModules](#gibargsfornotimpactedmodules)
+  - [gib.buildAll](#gibbuildall)
+  - [gib.buildDownstream](#gibbuilddownstream)
+  - [gib.buildUpstream](#gibbuildupstream)
+  - [gib.buildUpstreamMode](#gibbuildupstreammode)
+  - [gib.skipTestsForUpstreamModules](#gibskiptestsforupstreammodules)
+  - [gib.argsForUpstreamModules](#gibargsforupstreammodules)
   - [gib.forceBuildModules](#gibforcebuildmodules)
+  - [gib.excludeTransitiveModulesPackagedAs](#excludetransitivemodulespackagedas)
 
 - [Requirements](#requirements)
 
@@ -237,24 +242,26 @@ Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
 Maven pom properties configuration with default values is below:
 ```xml
 <properties>
-	<gib.enabled>true</gib.enabled>
-	<gib.repositorySshKey></gib.repositorySshKey>
-	<gib.disableBranchComparison>false</gib.disableBranchComparison>
-	<gib.referenceBranch>refs/remotes/origin/develop</gib.referenceBranch>
-	<gib.baseBranch>HEAD</gib.baseBranch>
-	<gib.uncommited>true</gib.uncommited>
-	<gib.untracked>true</gib.untracked>
-	<gib.skipTestsForNotImpactedModules>false</gib.skipTestsForNotImpactedModules>
-	<gib.argsForNotImpactedModules></gib.argsForNotImpactedModules>
-	<gib.buildAll>false</gib.buildAll>
-	<gib.forceBuildModules></gib.forceBuildModules>
-	<gib.excludeTransitiveModulesPackagedAs></gib.excludeTransitiveModulesPackagedAs>
-	<gib.compareToMergeBase>true</gib.compareToMergeBase>
-	<gib.fetchBaseBranch>false</gib.fetchBaseBranch>
-	<gib.fetchReferenceBranch>false</gib.fetchReferenceBranch>
-	<gib.excludePathRegex>(?!x)x</gib.excludePathRegex>
-	<gib.failOnMissingGitDir>true</gib.failOnMissingGitDir>
-	<gib.failOnError>true</gib.failOnError>
+    <gib.enabled>true</gib.enabled>
+    <gib.disableBranchComparison>false</gib.disableBranchComparison>
+    <gib.referenceBranch>refs/remotes/origin/develop</gib.referenceBranch>
+    <gib.fetchReferenceBranch>false</gib.fetchReferenceBranch>
+    <gib.baseBranch>HEAD</gib.baseBranch>
+    <gib.fetchBaseBranch>false</gib.fetchBaseBranch>
+    <gib.compareToMergeBase>true</gib.compareToMergeBase>
+    <gib.uncommited>true</gib.uncommited>
+    <gib.untracked>true</gib.untracked>
+    <gib.excludePathRegex>(?!x)x</gib.excludePathRegex>
+    <gib.buildAll>false</gib.buildAll>
+    <gib.buildDownstream>always</gib.buildDownstream>
+    <gib.buildUpstream>derived</gib.buildUpstream>
+    <gib.buildUpstreamMode>changed</gib.buildUpstreamMode>
+    <gib.skipTestsForUpstreamModules>false</gib.skipTestsForUpstreamModules>
+    <gib.argsForUpstreamModules></gib.argsForUpstreamModules>
+    <gib.forceBuildModules></gib.forceBuildModules>
+    <gib.excludeTransitiveModulesPackagedAs></gib.excludeTransitiveModulesPackagedAs>
+    <gib.failOnMissingGitDir>true</gib.failOnMissingGitDir>
+    <gib.failOnError>true</gib.failOnError>
 </properties>
 ```
 
@@ -279,22 +286,72 @@ Detects changed files that have not yet been committed. This does **not** includ
 
 Detects files that are not yet tracked by git (see `git status` manual). This does **not** include _uncommitted_ files. A new file is not _untracked_ anymore after it is added to the index.
 
-### gib.skipTestsForNotImpactedModules
+### gib.buildAll
 
-In conjunction with `-am` or `gib.buildAll=true` this property disables the compilation/execution of tests for modules that have _not_ been changed by adding `maven.test.skip=true`. In case a not impacted module produces a test jar just the test _execution_ is disbled via `skipTests=true`.
+Builds all modules, including upstream modules (see also `gib.buildUpstream`). Can be used to (temporarily) override the reduction of modules to build.
 
-Can be combined with `gib.argsForNotImpactedModules`.
+Can be combined/useful with `gib.skipTestsForUpstreamModules` and/or `gib.argsForUpstreamModules`.
 
-### gib.argsForNotImpactedModules
+### gib.buildDownstream
 
-In conjunction with `-am` or `gib.buildAll=true` this property allows adding arguments/properties for modules that have _not_ been changed to futher reduce overhead, e.g. skip Checkstyle or Enforcer plugin.
+Controls whether or not to build downstream modules (= modules that depend on the modules GIB detected as changed):
+
+- `always` or `true` (default value): always build downstream modules
+- `derived`: only build downstream modules if `mvn -amd` is called
+- `never` or `false`: never build downstream modules
+
+Since: 3.8
+
+### gib.buildUpstream
+
+Controls whether or not to build upstream modules (= dependencies and parents of the modules GIB has determined to build):
+
+- `always` or `true`: always build upstream modules
+- `derived` (default value): only build upstream modules if `mvn -am` is called
+- `never` or `false`: never build upstream modules
+
+See also `gib.buildUpstreamMode`.
+
+Since: 3.8
+
+### gib.buildUpstreamMode
+
+This property controls which upstream modules to build (_if_ at all building upstream modules, see `gib.buildUpstream`):
+
+- `changed` (default value): selects only upstream modules of the _directly changed_ modules
+- `impacted`: like `changed` but also selects upstream modules of modules _that depend on_ the directly changed modules (in other words: upstream modules of the downstream modules of the changed modules)
+
+`changed` is a subset of `impacted`.
+
+`impacted` may seem odd at first, but it does come in handy in certain scenarios, e.g. a Jenkins PR job that locally merges target branch into the PR branch before building.
+Here it might be required to freshly compile upstream modules of not directly changed modules to avoid compile errors or test failures which originate from the target branch.
+
+Both strategies can and usually should be combined with `gib.skipTestsForUpstreamModules` and/or `gib.argsForUpstreamModules`.
+
+Note: _Before_ 3.8, GIB did non have this property and was implicity applying the `impacted` strategy, see also #44.
+
+Since: 3.8
+
+### gib.skipTestsForUpstreamModules
+
+This property disables the compilation/execution of tests for upstream modules by adding `maven.test.skip=true`. In case an upstream module produces a test jar just the test _execution_ is disabled via `skipTests=true`.
+
+See `gib.buildUpstream` or `gib.buildAll` to learn when upstream modules are built.
+
+Can be combined with `gib.argsForUpstreamModules`.
+
+### gib.argsForUpstreamModules
+
+This property allows adding arbitrary arguments/properties for upstream modules to futher reduce overhead, e.g. skip Checkstyle or Enforcer plugin.
 Arguments have to be sparated with a single space character and values are optional. Example:
 
 ```
-mvn clean install -am -Dgib.argsForNotImpactedModules='-Denforcer.skip -Dcheckstyle.skip=true'
+mvn clean install -am -Dgib.argsForUpstreamModules='-Denforcer.skip -Dcheckstyle.skip=true'
 ```
 
-Can be combined with `gib.skipTestsForNotImpactedModules`.
+See `gib.buildUpstream` or `gib.buildAll` to learn when upstream modules are built.
+
+Can be combined with `gib.skipTestsForUpstreamModules`.
 
 ### gib.forceBuildModules
 
@@ -310,7 +367,7 @@ Regular expressions are also supported in each comma separated part, e.g.:
 mvn clean install -Dgib.forceBuildModules=unchanged-module-.*,another-module
 ```
 
-Each of these modules is subject to `argsForNotImpactedModules` and `skipTestsForNotImpactedModules`.
+Each of these modules is subject to `argsForUpstreamModules` and `skipTestsForUpstreamModules`.
 
 This property has no effect in case `buildAll` is enabled.
 
diff --git a/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/Configuration.java b/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/Configuration.java
index b533f09..46fe65c 100644
--- a/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/Configuration.java
+++ b/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/Configuration.java
@@ -8,7 +8,6 @@ import org.apache.maven.execution.MavenSession;
 import static java.util.stream.Collectors.collectingAndThen;
 import static java.util.stream.Collectors.toList;
 
-import java.nio.file.Path;
 import java.util.AbstractMap;
 import java.util.Arrays;
 import java.util.Collections;
@@ -16,6 +15,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Properties;
 import java.util.Set;
@@ -32,25 +32,24 @@ import javax.inject.Singleton;
 
 public class Configuration {
 
-    private static final List<String> alsoMakeBehaviours = Arrays.asList(
-            MavenExecutionRequest.REACTOR_MAKE_UPSTREAM, MavenExecutionRequest.REACTOR_MAKE_BOTH);
-
-    public final Optional<Path> key;
     public final boolean disableBranchComparison;
     public final String referenceBranch;
+    public final boolean fetchReferenceBranch;
     public final String baseBranch;
+    public final boolean fetchBaseBranch;
+    public final boolean compareToMergeBase;
     public final boolean uncommited;
     public final boolean untracked;
-    public final boolean makeUpstream;
-    public final boolean skipTestsForNotImpactedModules;
-    public final Map<String, String> argsForNotImpactedModules;
+    public final Predicate<String> excludePathRegex;
+
     public final boolean buildAll;
+    public final boolean buildDownstream;
+    public final BuildUpstreamMode buildUpstreamMode;
+    public final boolean skipTestsForUpstreamModules;
+    public final Map<String, String> argsForUpstreamModules;
     public final List<Pattern> forceBuildModules;
     public final List<String> excludeTransitiveModulesPackagedAs;
-    public final boolean compareToMergeBase;
-    public final boolean fetchBaseBranch;
-    public final boolean fetchReferenceBranch;
-    public final Predicate<String> excludePathRegex;
+
     public final boolean failOnMissingGitDir;
     public final boolean failOnError;
 
@@ -58,20 +57,28 @@ public class Configuration {
         Properties projectProperties = session.getTopLevelProject().getProperties();
         checkProperties(projectProperties);
 
-        key = parseKey(session, projectProperties);
+        // change detection config
+
         disableBranchComparison = Boolean.valueOf(Property.disableBranchComparison.getValue(projectProperties));
         referenceBranch = Property.referenceBranch.getValue(projectProperties);
+        fetchReferenceBranch = Boolean.valueOf(Property.fetchReferenceBranch.getValue(projectProperties));
         baseBranch = Property.baseBranch.getValue(projectProperties);
+        fetchBaseBranch = Boolean.valueOf(Property.fetchBaseBranch.getValue(projectProperties));
+        compareToMergeBase = Boolean.valueOf(Property.compareToMergeBase.getValue(projectProperties));
         uncommited = Boolean.valueOf(Property.uncommited.getValue(projectProperties));
         untracked = Boolean.valueOf(Property.untracked.getValue(projectProperties));
-        makeUpstream = alsoMakeBehaviours.contains(session.getRequest().getMakeBehavior());
-        skipTestsForNotImpactedModules = Boolean.valueOf(Property.skipTestsForNotImpactedModules.getValue(projectProperties));
+        excludePathRegex = compilePattern(Property.excludePathRegex, projectProperties).asPredicate();
 
-        argsForNotImpactedModules = parseDelimited(Property.argsForNotImpactedModules.getValue(projectProperties), " ")
-                .map(Configuration::keyValueStringToEntry)
-                .collect(collectingAndThen(toLinkedMap(), Collections::unmodifiableMap));
+        // build config
 
         buildAll = Boolean.valueOf(Property.buildAll.getValue(projectProperties));
+        buildDownstream = isBuildStreamActive(Property.buildDownstream, projectProperties, session, MavenExecutionRequest.REACTOR_MAKE_DOWNSTREAM);
+        buildUpstreamMode = parseBuildUpstreamMode(session, projectProperties);
+        skipTestsForUpstreamModules = Boolean.valueOf(Property.skipTestsForUpstreamModules.getValue(projectProperties));
+
+        argsForUpstreamModules = parseDelimited(Property.argsForUpstreamModules.getValue(projectProperties), " ")
+                .map(Configuration::keyValueStringToEntry)
+                .collect(collectingAndThen(toLinkedMap(), Collections::unmodifiableMap));
 
         forceBuildModules = parseDelimited(Property.forceBuildModules.getValue(projectProperties), ",")
                 .map(str -> compilePattern(str, Property.forceBuildModules))
@@ -80,10 +87,8 @@ public class Configuration {
         excludeTransitiveModulesPackagedAs = parseDelimited(Property.excludeTransitiveModulesPackagedAs.getValue(projectProperties), ",")
                 .collect(collectingAndThen(toList(), Collections::unmodifiableList));
 
-        compareToMergeBase = Boolean.valueOf(Property.compareToMergeBase.getValue(projectProperties));
-        fetchReferenceBranch = Boolean.valueOf(Property.fetchReferenceBranch.getValue(projectProperties));
-        fetchBaseBranch = Boolean.valueOf(Property.fetchBaseBranch.getValue(projectProperties));
-        excludePathRegex = compilePattern(Property.excludePathRegex, projectProperties).asPredicate();
+        // error handling config
+
         failOnMissingGitDir = Boolean.valueOf(Property.failOnMissingGitDir.getValue(projectProperties));
         failOnError = Boolean.valueOf(Property.failOnError.getValue(projectProperties));
     }
@@ -100,7 +105,8 @@ public class Configuration {
 
     private static void checkProperties(Properties projectProperties) {
         Set<String> availablePropertyNames = Arrays.stream(Property.values())
-                .map(Property::fullName)
+                .flatMap(p -> Stream.of(p.fullName(), p.deprecatedFullName()))
+                .filter(Objects::nonNull)
                 .collect(Collectors.toSet());
         String invalidPropertyNames = Stream.concat(System.getProperties().keySet().stream(), projectProperties.keySet().stream())
                 .distinct()
@@ -113,13 +119,32 @@ public class Configuration {
         }
     }
 
-    private static Optional<Path> parseKey(MavenSession session, Properties projectProperties) {
-        String keyOptionValue = Property.repositorySshKey.getValue(projectProperties);
-        if (keyOptionValue != null && ! keyOptionValue.isEmpty()) {
-            Path pomDir = session.getCurrentProject().getBasedir().toPath();
-            return Optional.of(pomDir.resolve(keyOptionValue).toAbsolutePath().normalize());
-        } else {
-            return Optional.empty();
+    private static BuildUpstreamMode parseBuildUpstreamMode(MavenSession session, Properties projectProperties) {
+        if (!isBuildStreamActive(Property.buildUpstream, projectProperties, session, MavenExecutionRequest.REACTOR_MAKE_UPSTREAM)) {
+            return BuildUpstreamMode.NONE;
+        }
+        try {
+            String propertyValue = Optional.ofNullable(Property.buildUpstreamMode.getValue(projectProperties)).orElse("");
+            return BuildUpstreamMode.valueOf(propertyValue.toUpperCase());
+        } catch (IllegalArgumentException e) {
+            throw new IllegalArgumentException("GIB property " + Property.buildUpstreamMode.fullName() + " defines an invalid mode", e);
+        }
+    }
+
+    private static boolean isBuildStreamActive(Property property, Properties projectProperties, MavenSession session, String expectedMakeBehavior) {
+        switch (property.getValue(projectProperties)) {
+            case "derived":
+                String actualMakeBehavior = session.getRequest().getMakeBehavior();
+                return expectedMakeBehavior.equals(actualMakeBehavior) || MavenExecutionRequest.REACTOR_MAKE_BOTH.equals(actualMakeBehavior);
+            case "always":
+            case "true":
+                return true;
+            case "never":
+            case "false":
+                return false;
+            default:
+                throw new IllegalArgumentException(
+                        "GIB property " + property.fullName() + " defines an invalid value: " + property.getValue(projectProperties));
         }
     }
 
@@ -154,6 +179,12 @@ public class Configuration {
         return compilePattern(property.getValue(projectProperties), property);
     }
 
+    public static enum BuildUpstreamMode {
+        NONE,
+        CHANGED,
+        IMPACTED;
+    }
+
     @Singleton
     @Named("gib.configurationProvider")
     public static class Provider implements javax.inject.Provider<Configuration> {
diff --git a/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemover.java b/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemover.java
index cf79371..0d6c051 100644
--- a/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemover.java
+++ b/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemover.java
@@ -36,52 +36,68 @@ class UnchangedProjectsRemover {
         Set<MavenProject> changed = changedProjects.get();
         printDelimiter();
         logProjects(changed, "Changed Artifacts:");
-        Set<MavenProject> impacted = changed.stream()
-            .flatMap(this::streamProjectWithDependents)
-            .collect(Collectors.toCollection(LinkedHashSet::new));
-        if (!configProvider.get().buildAll) {
-            Set<MavenProject> rebuild = getRebuildProjects(impacted);
-            if (rebuild.isEmpty()) {
-                logger.info("No changed artifacts to build. Executing validate goal on current project only.");
-                mavenSession.setProjects(Collections.singletonList(mavenSession.getCurrentProject()));
-                mavenSession.getGoals().clear();
-                mavenSession.getGoals().add("validate");
-            } else if (!configProvider.get().forceBuildModules.isEmpty()) {
-                Stream<MavenProject> forceBuildModules = mavenSession.getProjects().stream()
-                        .filter(p -> matchesAny(p.getArtifactId(), configProvider.get().forceBuildModules))
-                        .filter(p -> !rebuild.contains(p))
-                        .map(this::applyNotImpactedModuleArgs);
-                mavenSession.setProjects(
-                        Stream.concat(forceBuildModules, rebuild.stream()).collect(Collectors.toList()));
-            } else {
-                mavenSession.setProjects(new ArrayList<>(rebuild));
-            }
+
+        // important: buildAll *always* need impacted incl. donwstream, otherwise applyNotImpactedModuleArgs() might disable tests etc. for downstream modules!
+        Configuration cfg = configProvider.get();
+        Set<MavenProject> impacted = cfg.buildAll || cfg.buildDownstream
+                ? changed.stream()
+                        .flatMap(this::streamProjectWithDownstreamProjects)
+                        .collect(Collectors.toCollection(LinkedHashSet::new))
+                : changed;
+
+        if (!cfg.buildAll) {
+            modifyProjectList(changed, impacted);
         } else {
             mavenSession.getProjects().stream()
                     .filter(p -> !impacted.contains(p))
-                    .forEach(this::applyNotImpactedModuleArgs);
+                    .forEach(this::applyUpstreamModuleArgs);
         }
     }
 
-    private Set<MavenProject> getRebuildProjects(Set<MavenProject> changedProjects) {
-        if (configProvider.get().makeUpstream) {
-            return Stream.concat(changedProjects.stream(), collectDependencies(changedProjects))
-                .collect(Collectors.toCollection(LinkedHashSet::new));
+    private void modifyProjectList(Set<MavenProject> changed, Set<MavenProject> impacted) {
+        Set<MavenProject> rebuild = getRebuildProjects(changed, impacted);
+        if (rebuild.isEmpty()) {
+            logger.info("No changed artifacts to build. Executing validate goal on current project only.");
+            mavenSession.setProjects(Collections.singletonList(mavenSession.getCurrentProject()));
+            mavenSession.getGoals().clear();
+            mavenSession.getGoals().add("validate");
+        } else if (!configProvider.get().forceBuildModules.isEmpty()) {
+            Stream<MavenProject> forceBuildModules = mavenSession.getProjects().stream()
+                    .filter(p -> matchesAny(p.getArtifactId(), configProvider.get().forceBuildModules))
+                    .filter(p -> !rebuild.contains(p))
+                    .map(this::applyUpstreamModuleArgs);
+            mavenSession.setProjects(
+                    Stream.concat(forceBuildModules, rebuild.stream()).collect(Collectors.toList()));
         } else {
-            return changedProjects;
+            mavenSession.setProjects(new ArrayList<>(rebuild));
         }
     }
 
-    private Stream<MavenProject> collectDependencies(Set<MavenProject> changedProjects) {
-        return changedProjects.stream()
-                .flatMap(this::streamProjectWithDependencies)
-                .filter(p -> ! changedProjects.contains(p))
-                .map(this::applyNotImpactedModuleArgs);
+    private Set<MavenProject> getRebuildProjects(Set<MavenProject> changed, Set<MavenProject> impacted) {
+        Set<MavenProject> upstreamRequiringProjects;
+        switch (configProvider.get().buildUpstreamMode) {
+            case NONE:
+                return impacted;    // just use impacted without any further processing
+            case CHANGED:
+                upstreamRequiringProjects = changed;
+                break;
+            case IMPACTED:
+                upstreamRequiringProjects = impacted;
+                break;
+            default:
+                throw new IllegalStateException("Unsupported BuildUpstreamMode: " + configProvider.get().buildUpstreamMode);
+        }
+        Stream<MavenProject> upstreamProjects = upstreamRequiringProjects.stream()
+                .flatMap(this::streamUpstreamProjects)
+                .filter(p -> ! changed.contains(p))
+                .map(this::applyUpstreamModuleArgs);
+        return Stream.concat(impacted.stream(), upstreamProjects)
+                .collect(Collectors.toCollection(LinkedHashSet::new));
     }
 
-    private MavenProject applyNotImpactedModuleArgs(MavenProject mavenProject) {
+    private MavenProject applyUpstreamModuleArgs(MavenProject mavenProject) {
         final Properties projectProperties = mavenProject.getProperties();
-        if (configProvider.get().skipTestsForNotImpactedModules) {
+        if (configProvider.get().skipTestsForUpstreamModules) {
             if (projectDeclaresTestJarGoal(mavenProject)) {
                 logger.debug("{}: {}", mavenProject.getArtifactId(), TEST_JAR_DETECTED);
                 projectProperties.setProperty(MAVEN_TEST_SKIP_EXEC, Boolean.TRUE.toString());
@@ -89,12 +105,12 @@ class UnchangedProjectsRemover {
                 projectProperties.setProperty(MAVEN_TEST_SKIP, Boolean.TRUE.toString());
             }
         }
-        configProvider.get().argsForNotImpactedModules.forEach(projectProperties::setProperty);
+        configProvider.get().argsForUpstreamModules.forEach(projectProperties::setProperty);
         return mavenProject;
     }
 
-    private boolean projectDeclaresTestJarGoal(MavenProject mavenProject) {
-        return mavenProject.getBuildPlugins().stream()
+    private boolean projectDeclaresTestJarGoal(MavenProject project) {
+        return project.getBuildPlugins().stream()
                 .flatMap(p -> p.getExecutions().stream())
                 .flatMap(e -> e.getGoals().stream())
                 .anyMatch(GOAL_TEST_JAR::equals);
@@ -111,17 +127,15 @@ class UnchangedProjectsRemover {
         logger.info("------------------------------------------------------------------------");
     }
 
-    private Stream<MavenProject> streamProjectWithDependents(MavenProject project) {
+    private Stream<MavenProject> streamProjectWithDownstreamProjects(MavenProject project) {
         return Stream.concat(
             Stream.of(project),
             mavenSession.getProjectDependencyGraph().getDownstreamProjects(project, true).stream()
-                .filter(p -> !configProvider.get().excludeTransitiveModulesPackagedAs.contains(p.getPackaging())));
+                    .filter(p -> !configProvider.get().excludeTransitiveModulesPackagedAs.contains(p.getPackaging())));
     }
 
-    private Stream<MavenProject> streamProjectWithDependencies(MavenProject project) {
-        return Stream.concat(
-            Stream.of(project),
-            mavenSession.getProjectDependencyGraph().getUpstreamProjects(project, true).stream());
+    private Stream<MavenProject> streamUpstreamProjects(MavenProject project) {
+        return mavenSession.getProjectDependencyGraph().getUpstreamProjects(project, true).stream();
     }
 
     private boolean matchesAny(final String str, Collection<Pattern> patterns) {
diff --git a/src/main/java/com/vackosar/gitflowincrementalbuild/control/Property.java b/src/main/java/com/vackosar/gitflowincrementalbuild/control/Property.java
index 773fe36..0df4f12 100644
--- a/src/main/java/com/vackosar/gitflowincrementalbuild/control/Property.java
+++ b/src/main/java/com/vackosar/gitflowincrementalbuild/control/Property.java
@@ -3,34 +3,51 @@ package com.vackosar.gitflowincrementalbuild.control;
 import java.util.Optional;
 import java.util.Properties;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 public enum Property {
     enabled("true"),
-    repositorySshKey(""),
+
+
     disableBranchComparison("false"),
     referenceBranch("refs/remotes/origin/develop"),
+    fetchReferenceBranch("false"),
     baseBranch("HEAD"),
+    fetchBaseBranch("false"),
+    compareToMergeBase("true"),
     uncommited("true"),
     untracked("true"),
-    skipTestsForNotImpactedModules("false"),
-    argsForNotImpactedModules(""),
+    excludePathRegex(Constants.NEVER_MATCH_REGEX),
+
     buildAll("false"),
+    buildDownstream("always"),
+    buildUpstream("derived"),
+    buildUpstreamMode("changed"),
+    skipTestsForUpstreamModules("false", "skipTestsForNotImpactedModules"),
+    argsForUpstreamModules("", "argsForNotImpactedModules"),
     forceBuildModules(""),
     excludeTransitiveModulesPackagedAs(""),
-    compareToMergeBase("true"),
-    fetchBaseBranch("false"),
-    fetchReferenceBranch("false"),
-    excludePathRegex(Constants.NEVER_MATCH_REGEX),
+
     failOnMissingGitDir("true"),
     failOnError("true");
 
     public static final String PREFIX = "gib.";
 
+    private static final Logger LOGGER = LoggerFactory.getLogger(Property.class);
+
     private final String fullName;
     private final String defaultValue;
+    private final String deprecatedFullName;
 
     Property(String defaultValue) {
+        this(defaultValue, null);
+    }
+
+    Property(String defaultValue, String deprecatedFullName) {
         this.fullName = PREFIX + name();
         this.defaultValue = defaultValue;
+        this.deprecatedFullName = deprecatedFullName != null ? PREFIX + deprecatedFullName : null;
     }
 
     private String exemplify() {
@@ -41,9 +58,28 @@ public enum Property {
         return fullName;
     }
 
+    public String deprecatedFullName() {
+        return deprecatedFullName;
+    }
+
     public String getValue(Properties projectProperties) {
-        return Optional.ofNullable(System.getProperty(fullName))
-                .orElseGet(() -> projectProperties.getProperty(fullName, defaultValue));
+        String value = getValue(fullName, projectProperties);
+        if (value != null) {
+            return value;
+        }
+        if (deprecatedFullName != null) {
+            value = getValue(deprecatedFullName, projectProperties);
+            if (value != null) {
+                LOGGER.warn("{} has been replaced with {} and will be removed in an upcoming release. Please adjust your configuration!",
+                        deprecatedFullName, fullName);
+                return value;
+            }
+        }
+        return defaultValue;
+    }
+
+    private static String getValue(String fullName, Properties projectProperties) {
+        return Optional.ofNullable(System.getProperty(fullName)).orElseGet(() -> projectProperties.getProperty(fullName));
     }
 
     public static String exemplifyAll() {


Test patch:
diff --git a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/ConfigurationTest.java b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/ConfigurationTest.java
index d2df5b9..3462d76 100644
--- a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/ConfigurationTest.java
+++ b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/ConfigurationTest.java
@@ -3,7 +3,12 @@ package com.vackosar.gitflowincrementalbuild.boundary;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.util.Arrays;
@@ -26,6 +31,7 @@ import org.mockito.Mock;
 import org.mockito.runners.MockitoJUnitRunner;
 
 import com.google.common.collect.ImmutableMap;
+import com.vackosar.gitflowincrementalbuild.boundary.Configuration.BuildUpstreamMode;
 import com.vackosar.gitflowincrementalbuild.control.Property;
 
 /**
@@ -105,13 +111,23 @@ public class ConfigurationTest {
         assertFalse(Configuration.isEnabled(mavenSessionMock));
     }
 
+    @Test
+    public void argsForUpstreamModules() {
+        System.setProperty(Property.argsForUpstreamModules.fullName(), "x=true a=false");
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertEquals(ImmutableMap.of("x", "true", "a", "false"), configuration.argsForUpstreamModules);
+    }
+
+    // deprecated old name of argsForUpstreamModules
     @Test
     public void argsForNotImpactedModules() {
-        System.setProperty(Property.argsForNotImpactedModules.fullName(), "x=true a=false");
+        System.setProperty(Property.argsForUpstreamModules.deprecatedFullName(), "x=true a=false");
 
         Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
 
-        assertEquals(ImmutableMap.of("x", "true", "a", "false"), configuration.argsForNotImpactedModules);
+        assertEquals(ImmutableMap.of("x", "true", "a", "false"), configuration.argsForUpstreamModules);
     }
 
     @Test
@@ -146,4 +162,224 @@ public class ConfigurationTest {
 
         new Configuration.Provider(mavenSessionMock).get();
     }
+
+    // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // tests for configuration.buildUpstreamMode (which is calculated from two(!) properties: buildUpstream and buildUpstreamMode)
+
+    @Test
+    public void buildUpstreamMode_never() {
+        System.setProperty(Property.buildUpstream.fullName(), "never");
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertSame(BuildUpstreamMode.NONE, configuration.buildUpstreamMode);
+        verify(mavenExecutionRequestMock, never()).getMakeBehavior();
+    }
+
+    @Test
+    public void buildUpstreamMode_false() {
+        System.setProperty(Property.buildUpstream.fullName(), "false");
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertSame(BuildUpstreamMode.NONE, configuration.buildUpstreamMode);
+        verify(mavenExecutionRequestMock, never()).getMakeBehavior();
+    }
+
+    @Test
+    public void buildUpstreamMode_always() {
+        System.setProperty(Property.buildUpstream.fullName(), "always");
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertSame(BuildUpstreamMode.CHANGED, configuration.buildUpstreamMode);
+        verify(mavenExecutionRequestMock, never()).getMakeBehavior();
+    }
+
+    @Test
+    public void buildUpstreamMode_true() {
+        System.setProperty(Property.buildUpstream.fullName(), "true");
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertSame(BuildUpstreamMode.CHANGED, configuration.buildUpstreamMode);
+        verify(mavenExecutionRequestMock, never()).getMakeBehavior();
+    }
+
+    @Test
+    public void buildUpstreamMode_unknown() {
+        System.setProperty(Property.buildUpstream.fullName(), "foo");
+        thrown.expect(IllegalArgumentException.class);
+        thrown.expectMessage(Property.buildUpstream.fullName());
+
+        new Configuration.Provider(mavenSessionMock).get();
+    }
+
+    // tests for mode value 'derived' (default value)
+
+    @Test
+    public void buildUpstreamMode_derived_noMake() {
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertSame(BuildUpstreamMode.NONE, configuration.buildUpstreamMode);
+    }
+
+    @Test
+    public void buildUpstreamMode_derived_makeUpstream() {
+        when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_UPSTREAM);
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertSame(BuildUpstreamMode.CHANGED, configuration.buildUpstreamMode);
+    }
+
+    @Test
+    public void buildUpstreamMode_derived_makeBoth() {
+        when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_BOTH);
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertSame(BuildUpstreamMode.CHANGED, configuration.buildUpstreamMode);
+    }
+
+    @Test
+    public void buildUpstreamMode_derived_makeDownstream() {
+        when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_DOWNSTREAM);
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertSame(BuildUpstreamMode.NONE, configuration.buildUpstreamMode);
+    }
+
+    @Test
+    public void buildUpstreamMode_derived_makeUpstream_impacted() {
+        System.setProperty(Property.buildUpstreamMode.fullName(), "impacted");
+        when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_UPSTREAM);
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertSame(BuildUpstreamMode.IMPACTED, configuration.buildUpstreamMode);
+    }
+
+    @Test
+    public void buildUpstreamMode_derived_makeUpstream_unknown() {
+        System.setProperty(Property.buildUpstreamMode.fullName(), "foo");
+        when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_UPSTREAM);
+        thrown.expect(IllegalArgumentException.class);
+        thrown.expectMessage(Property.buildUpstreamMode.fullName());
+        thrown.expectCause(IsInstanceOf.instanceOf(IllegalArgumentException.class));
+
+        new Configuration.Provider(mavenSessionMock).get();
+    }
+
+    // just an example to show 'derived' can also be set explicitely
+    @Test
+    public void buildUpstreamMode_derivedExplicit_makeUpstream() {
+        System.setProperty(Property.buildUpstream.fullName(), "derived");
+        when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_UPSTREAM);
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertSame(BuildUpstreamMode.CHANGED, configuration.buildUpstreamMode);
+    }
+
+    // ///////////////////////////////////////
+    // tests for configuration.buildDownstream
+
+    // 'always' is default
+    @Test
+    public void buildDownstream() {
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertTrue(configuration.buildDownstream);
+        verify(mavenExecutionRequestMock, times(1)).getMakeBehavior();  // called once for buildDownstreamMode
+    }
+
+    @Test
+    public void buildDownstream_never() {
+        System.setProperty(Property.buildDownstream.fullName(), "never");
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertFalse(configuration.buildDownstream);
+        verify(mavenExecutionRequestMock, times(1)).getMakeBehavior();  // called once for buildDownstreamMode
+    }
+
+    @Test
+    public void buildDownstream_false() {
+        System.setProperty(Property.buildDownstream.fullName(), "false");
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertFalse(configuration.buildDownstream);
+        verify(mavenExecutionRequestMock, times(1)).getMakeBehavior();  // called once for buildDownstreamMode
+    }
+
+    @Test
+    public void buildDownstream_always() {
+        System.setProperty(Property.buildDownstream.fullName(), "always");
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertTrue(configuration.buildDownstream);
+        verify(mavenExecutionRequestMock, times(1)).getMakeBehavior();  // called once for buildDownstreamMode
+    }
+
+    @Test
+    public void buildDownstream_true() {
+        System.setProperty(Property.buildDownstream.fullName(), "true");
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertTrue(configuration.buildDownstream);
+        verify(mavenExecutionRequestMock, times(1)).getMakeBehavior();  // called once for buildDownstreamMode
+    }
+
+    @Test
+    public void buildDownstream_unknown() {
+        System.setProperty(Property.buildDownstream.fullName(), "foo");
+        thrown.expect(IllegalArgumentException.class);
+        thrown.expectMessage(Property.buildDownstream.fullName());
+
+        new Configuration.Provider(mavenSessionMock).get();
+    }
+
+    @Test
+    public void buildDownstream_derived_noMake() {
+        System.setProperty(Property.buildDownstream.fullName(), "derived");
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertFalse(configuration.buildDownstream);
+    }
+
+    @Test
+    public void buildDownstream_derived_makeDownstream() {
+        System.setProperty(Property.buildDownstream.fullName(), "derived");
+        when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_DOWNSTREAM);
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertTrue(configuration.buildDownstream);
+    }
+
+    @Test
+    public void buildDownstream_derived_makeBoth() {
+        System.setProperty(Property.buildDownstream.fullName(), "derived");
+        when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_BOTH);
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertTrue(configuration.buildDownstream);
+    }
+
+    @Test
+    public void buildDownstream_derived_makeUpstream() {
+        System.setProperty(Property.buildDownstream.fullName(), "derived");
+        when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_UPSTREAM);
+
+        Configuration configuration = new Configuration.Provider(mavenSessionMock).get();
+
+        assertFalse(configuration.buildDownstream);
+    }
 }
diff --git a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/MavenIntegrationTest.java b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/MavenIntegrationTest.java
index 90c200e..b52a925 100644
--- a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/MavenIntegrationTest.java
+++ b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/MavenIntegrationTest.java
@@ -103,7 +103,7 @@ public class MavenIntegrationTest extends BaseRepoTest {
 
     @Test
     public void buildAllSkipTest() throws Exception {
-        final String output = executeBuild(prop(Property.buildAll, "true"), prop(Property.skipTestsForNotImpactedModules, "true"));
+        final String output = executeBuild(prop(Property.buildAll, "true"), prop(Property.skipTestsForUpstreamModules, "true"));
 
         Assert.assertTrue(output.contains(" child1"));
         Assert.assertTrue(output.contains(" child2"));
@@ -156,7 +156,7 @@ public class MavenIntegrationTest extends BaseRepoTest {
 
     @Test
     public void buildWithAlsoMakeSkip() throws Exception {
-        final String output = executeBuild("-am", prop(Property.skipTestsForNotImpactedModules, "true"));
+        final String output = executeBuild("-am", prop(Property.skipTestsForUpstreamModules, "true"));
 
         Assert.assertFalse(output.contains(" child1"));
         Assert.assertFalse(output.contains(" child2"));
@@ -187,6 +187,22 @@ public class MavenIntegrationTest extends BaseRepoTest {
         Assert.assertTrue(output.contains(" subchild41"));
     }
 
+    @Test
+    public void buildWithAlsoMakeDependents() throws Exception {
+        final String output = executeBuild("-amd", prop(Property.buildDownstream, "derived"));
+
+        Assert.assertFalse(output.contains(" child1"));
+        Assert.assertFalse(output.contains(" child2"));
+        Assert.assertFalse(output.contains(" subchild1"));
+        Assert.assertFalse(output.contains(" subchild42"));
+        Assert.assertFalse(output.contains(" child6"));
+
+        Assert.assertTrue(output.contains(" subchild2"));
+        Assert.assertTrue(output.contains(" child3"));
+        Assert.assertTrue(output.contains(" child4"));
+        Assert.assertTrue(output.contains(" subchild41"));
+    }
+
     private String executeBuild(String... args) throws IOException, InterruptedException {
         final List<String> commandBase = ProcessUtils.cmdArgs("mvn", "-e", "package", localRepoArg, gibVersionArg);
         final List<String> commandBaseWithFile;
diff --git a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemoverTest.java b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemoverTest.java
index 2bf64c1..f5abe38 100644
--- a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemoverTest.java
+++ b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemoverTest.java
@@ -7,6 +7,7 @@ import static org.junit.Assert.assertEquals;
 import static org.mockito.Matchers.anyListOf;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.io.File;
@@ -16,12 +17,17 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
 
 import org.apache.maven.execution.MavenExecutionRequest;
 import org.apache.maven.execution.MavenSession;
 import org.apache.maven.execution.ProjectDependencyGraph;
+import org.apache.maven.model.Plugin;
+import org.apache.maven.model.PluginExecution;
 import org.apache.maven.project.MavenProject;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.junit.Before;
@@ -29,10 +35,10 @@ import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
-import org.mockito.Mockito;
 import org.mockito.runners.MockitoJUnitRunner;
 import org.powermock.reflect.Whitebox;
 
+import com.google.common.collect.ImmutableMap;
 import com.vackosar.gitflowincrementalbuild.control.ChangedProjects;
 import com.vackosar.gitflowincrementalbuild.control.Property;
 
@@ -69,7 +75,7 @@ public class UnchangedProjectsRemoverTest {
     private final List<MavenProject> projects = new ArrayList<>(); 
     private final Set<MavenProject> changedProjects = new LinkedHashSet<>();
     private final Properties projectProperties = new Properties();
-    
+
     @Before
     public void setup() throws GitAPIException, IOException {
         when(mavenProjectMock.getProperties()).thenReturn(projectProperties);
@@ -85,55 +91,256 @@ public class UnchangedProjectsRemoverTest {
         when(mavenSessionMock.getProjectDependencyGraph()).thenReturn(projectDependencyGraphMock);
         when(changedProjectsMock.get()).thenReturn(changedProjects);
 
+        when(mavenSessionMock.getGoals()).thenReturn(new ArrayList<>());
+
         Whitebox.setInternalState(underTest, new Configuration.Provider(mavenSessionMock));
     }
-    
+
+    @Test
+    public void nothingChanged() throws GitAPIException, IOException {
+        addModuleMock(ARTIFACT_ID_2, false);
+
+        underTest.act();
+
+        assertEquals("Unexpected goal", Collections.singletonList("validate"), mavenSessionMock.getGoals());
+
+        verify(mavenSessionMock).setProjects(Collections.singletonList(mavenProjectMock));
+    }
+
     @Test
     public void singleChanged() throws GitAPIException, IOException {
         MavenProject changedModuleMock = addModuleMock(ARTIFACT_ID_2, true);
 
         underTest.act();
 
-        Mockito.verify(mavenSessionMock).setProjects(Collections.singletonList(changedModuleMock));
+        verify(mavenSessionMock).setProjects(Collections.singletonList(changedModuleMock));
+    }
+
+    @Test
+    public void singleChanged_buildUpstream() throws GitAPIException, IOException {
+        MavenProject changedModuleMock = addModuleMock(ARTIFACT_ID_2, true);
+
+        when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_UPSTREAM);
+
+        underTest.act();
+
+        verify(mavenSessionMock).setProjects(Arrays.asList(changedModuleMock, mavenProjectMock));
+
+        assertProjectPropertiesEqual(mavenProjectMock, Collections.emptyMap());
+        assertProjectPropertiesEqual(changedModuleMock, Collections.emptyMap());
+    }
+
+    @Test
+    public void singleChanged_buildUpstream_skipTestsForUpstreamModules() throws GitAPIException, IOException {
+        MavenProject changedModuleMock = addModuleMock(ARTIFACT_ID_2, true);
+
+        when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_UPSTREAM);
+
+        projectProperties.put(Property.skipTestsForUpstreamModules.fullName(), "true");
+
+        underTest.act();
+
+        verify(mavenSessionMock).setProjects(Arrays.asList(changedModuleMock, mavenProjectMock));
+
+        assertProjectPropertiesEqual(mavenProjectMock, ImmutableMap.of("maven.test.skip", "true"));
+        assertProjectPropertiesEqual(changedModuleMock, Collections.emptyMap());
+    }
+
+    @Test
+    public void singleChanged_buildUpstream_skipTestsForUpstreamModules_jarGoal() throws GitAPIException, IOException {
+        MavenProject changedModuleMock = addModuleMock(ARTIFACT_ID_2, true);
+
+        when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_UPSTREAM);
+
+        projectProperties.put(Property.skipTestsForUpstreamModules.fullName(), "true");
+
+        Plugin pluginMock = mock(Plugin.class);
+        PluginExecution execMock = mock(PluginExecution.class);
+        when(execMock.getGoals()).thenReturn(Collections.singletonList("test-jar"));
+        when(pluginMock.getExecutions()).thenReturn(Collections.singletonList(execMock));
+        when(mavenProjectMock.getBuildPlugins()).thenReturn(Collections.singletonList(pluginMock));
+
+        underTest.act();
+
+        verify(mavenSessionMock).setProjects(Arrays.asList(changedModuleMock, mavenProjectMock));
+
+        assertProjectPropertiesEqual(mavenProjectMock, ImmutableMap.of("skipTests", "true"));
+        assertProjectPropertiesEqual(changedModuleMock, Collections.emptyMap());
+    }
+
+    @Test
+    public void singleChanged_buildUpstream_argsForUpstreamModules() throws GitAPIException, IOException {
+        MavenProject changedModuleMock = addModuleMock(ARTIFACT_ID_2, true);
+
+        when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_UPSTREAM);
+
+        projectProperties.put(Property.argsForUpstreamModules.fullName(), "enforcer.skip=true argWithNoValue");
+
+        underTest.act();
+
+        verify(mavenSessionMock).setProjects(Arrays.asList(changedModuleMock, mavenProjectMock));
+
+        assertProjectPropertiesEqual(mavenProjectMock, ImmutableMap.of("enforcer.skip", "true", "argWithNoValue", ""));
+        assertProjectPropertiesEqual(changedModuleMock, Collections.emptyMap());
+    }
+
+    @Test
+    public void singleChanged_buildUpstream_modeChanged() throws GitAPIException, IOException {
+        MavenProject changedModuleMock = addModuleMock(ARTIFACT_ID_2, true);
+        MavenProject unchangedModuleMock = addModuleMock("unchanged-module", false);
+        MavenProject dependsOnBothModuleMock = addModuleMock("changed-and-unchanged-dependent", false);
+
+        when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_UPSTREAM);
+
+        setUpAndDownstreamsForBuildUpstreamModeTests(changedModuleMock, unchangedModuleMock, dependsOnBothModuleMock);
+
+        projectProperties.put(Property.buildUpstreamMode.fullName(), "changed");
+
+        underTest.act();
+
+        verify(mavenSessionMock).setProjects(Arrays.asList(changedModuleMock, dependsOnBothModuleMock, mavenProjectMock));
+
+        assertProjectPropertiesEqual(mavenProjectMock, Collections.emptyMap());
+        assertProjectPropertiesEqual(changedModuleMock, Collections.emptyMap());
+        assertProjectPropertiesEqual(unchangedModuleMock, Collections.emptyMap());
+        assertProjectPropertiesEqual(dependsOnBothModuleMock, Collections.emptyMap());
+    }
+
+    @Test
+    public void singleChanged_buildUpstream_modeChanged_argsForUpstreamModules() throws GitAPIException, IOException {
+        MavenProject changedModuleMock = addModuleMock(ARTIFACT_ID_2, true);
+        MavenProject unchangedModuleMock = addModuleMock("unchanged-module", false);
+        MavenProject dependsOnBothModuleMock = addModuleMock("changed-and-unchanged-dependent", false);
+
+        when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_UPSTREAM);
+
+        setUpAndDownstreamsForBuildUpstreamModeTests(changedModuleMock, unchangedModuleMock, dependsOnBothModuleMock);
+
+        projectProperties.put(Property.buildUpstreamMode.fullName(), "changed");
+        projectProperties.put(Property.argsForUpstreamModules.fullName(), "foo=bar");
+
+        underTest.act();
+
+        verify(mavenSessionMock).setProjects(Arrays.asList(changedModuleMock, dependsOnBothModuleMock, mavenProjectMock));
+
+        assertProjectPropertiesEqual(mavenProjectMock, ImmutableMap.of("foo", "bar"));
+        assertProjectPropertiesEqual(changedModuleMock, Collections.emptyMap());
+        assertProjectPropertiesEqual(unchangedModuleMock, Collections.emptyMap());
+        assertProjectPropertiesEqual(dependsOnBothModuleMock, Collections.emptyMap());
+    }
+
+    @Test
+    public void singleChanged_buildUpstream_modeImpacted() throws GitAPIException, IOException {
+        MavenProject changedModuleMock = addModuleMock(ARTIFACT_ID_2, true);
+        MavenProject unchangedModuleMock = addModuleMock("unchanged-module", false);
+        MavenProject dependsOnBothModuleMock = addModuleMock("changed-and-unchanged-dependent", false);
+
+        when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_UPSTREAM);
+
+        setUpAndDownstreamsForBuildUpstreamModeTests(changedModuleMock, unchangedModuleMock, dependsOnBothModuleMock);
+
+        projectProperties.put(Property.buildUpstreamMode.fullName(), "impacted");   // this is also the default value!
+
+        underTest.act();
+
+        verify(mavenSessionMock).setProjects(Arrays.asList(changedModuleMock, dependsOnBothModuleMock, mavenProjectMock, unchangedModuleMock));
+
+        assertProjectPropertiesEqual(mavenProjectMock, Collections.emptyMap());
+        assertProjectPropertiesEqual(changedModuleMock, Collections.emptyMap());
+        assertProjectPropertiesEqual(unchangedModuleMock, Collections.emptyMap());
+        assertProjectPropertiesEqual(dependsOnBothModuleMock, Collections.emptyMap());
     }
 
     @Test
-    public void singleChanged_alsoMake_argsForNotImpactedModules() throws GitAPIException, IOException {
+    public void singleChanged_buildUpstream_modeImpacted_argsForUpstreamModules() throws GitAPIException, IOException {
         MavenProject changedModuleMock = addModuleMock(ARTIFACT_ID_2, true);
+        MavenProject unchangedModuleMock = addModuleMock("unchanged-module", false);
+        MavenProject dependsOnBothModuleMock = addModuleMock("changed-and-unchanged-dependent", false);
 
         when(mavenExecutionRequestMock.getMakeBehavior()).thenReturn(MavenExecutionRequest.REACTOR_MAKE_UPSTREAM);
 
-        projectProperties.put(Property.argsForNotImpactedModules.fullName(), "enforcer.skip=true argWithNoValue");
+        setUpAndDownstreamsForBuildUpstreamModeTests(changedModuleMock, unchangedModuleMock, dependsOnBothModuleMock);
+
+        projectProperties.put(Property.buildUpstreamMode.fullName(), "impacted");   // is also the default value!
+        projectProperties.put(Property.argsForUpstreamModules.fullName(), "foo=bar");
 
         underTest.act();
 
-        Mockito.verify(mavenSessionMock).setProjects(Arrays.asList(changedModuleMock, mavenProjectMock));
-        
-        assertEquals("Unexpected value for enforcer.skip",
-                "true", mavenProjectMock.getProperties().getProperty("enforcer.skip"));
-        assertEquals("Unexpected value for argWithNoValue",
-                "", mavenProjectMock.getProperties().getProperty("argWithNoValue"));
-        assertEquals("Unexpected properties for unchanged project",
-                new Properties(), changedModuleMock.getProperties());
+        verify(mavenSessionMock).setProjects(Arrays.asList(changedModuleMock, dependsOnBothModuleMock, mavenProjectMock, unchangedModuleMock));
+
+        assertProjectPropertiesEqual(mavenProjectMock, ImmutableMap.of("foo", "bar"));
+        assertProjectPropertiesEqual(changedModuleMock, Collections.emptyMap());
+        assertProjectPropertiesEqual(unchangedModuleMock, ImmutableMap.of("foo", "bar"));
+        assertProjectPropertiesEqual(dependsOnBothModuleMock, Collections.emptyMap());
     }
 
     @Test
-    public void singleChanged_buildAll_argsForNotImpactedModules() throws GitAPIException, IOException {
+    public void singleChanged_buildAll_argsForUpstreamModules() throws GitAPIException, IOException {
         MavenProject changedModuleMock = addModuleMock(ARTIFACT_ID_2, true);
 
-        projectProperties.put(Property.argsForNotImpactedModules.fullName(), "enforcer.skip=true argWithNoValue");
+        projectProperties.put(Property.argsForUpstreamModules.fullName(), "enforcer.skip=true argWithNoValue");
         projectProperties.put(Property.buildAll.fullName(), "true");
 
         underTest.act();
 
-        Mockito.verify(mavenSessionMock, never()).setProjects(anyListOf(MavenProject.class));
-        
-        assertEquals("Unexpected value for enforcer.skip",
-                "true", mavenProjectMock.getProperties().getProperty("enforcer.skip"));
-        assertEquals("Unexpected value for argWithNoValue",
-                "", mavenProjectMock.getProperties().getProperty("argWithNoValue"));
-        assertEquals("Unexpected properties for unchanged project",
-                new Properties(), changedModuleMock.getProperties());
+        verify(mavenSessionMock, never()).setProjects(anyListOf(MavenProject.class));
+
+        assertProjectPropertiesEqual(mavenProjectMock, ImmutableMap.of("enforcer.skip", "true", "argWithNoValue", ""));
+        assertProjectPropertiesEqual(changedModuleMock, Collections.emptyMap());
+    }
+
+    @Test
+    public void singleChanged_buildDownstream_enabled() throws GitAPIException, IOException {
+        MavenProject changedModuleMock = addModuleMock(ARTIFACT_ID_2, true);
+        MavenProject dependentModuleMock = addModuleMock(ARTIFACT_ID_2 + "-dependent-jar", false);
+
+        setUpstreamProjects(dependentModuleMock, changedModuleMock, mavenProjectMock);
+        setDownstreamProjects(changedModuleMock, dependentModuleMock);
+        setDownstreamProjects(mavenProjectMock, changedModuleMock, dependentModuleMock);   // just for consistency
+
+        // buildDownstream is enabled by default!
+
+        underTest.act();
+
+        verify(mavenSessionMock).setProjects(Arrays.asList(changedModuleMock, dependentModuleMock));
+    }
+
+    @Test
+    public void singleChanged_buildDownstream_disabled() throws GitAPIException, IOException {
+        MavenProject changedModuleMock = addModuleMock(ARTIFACT_ID_2, true);
+        MavenProject dependentModuleMock = addModuleMock(ARTIFACT_ID_2 + "-dependent-jar", false);
+
+        setUpstreamProjects(dependentModuleMock, changedModuleMock, mavenProjectMock);
+        setDownstreamProjects(changedModuleMock, dependentModuleMock);
+        setDownstreamProjects(mavenProjectMock, changedModuleMock, dependentModuleMock);   // just for consistency
+
+        projectProperties.put(Property.buildDownstream.fullName(), "false");
+
+        underTest.act();
+
+        verify(mavenSessionMock).setProjects(Arrays.asList(changedModuleMock));
+    }
+
+    @Test
+    public void singleChanged_buildDownstream_disabled_buildAll_argsForUpstreamModules() throws GitAPIException, IOException {
+        MavenProject changedModuleMock = addModuleMock(ARTIFACT_ID_2, true);
+        MavenProject dependentModuleMock = addModuleMock(ARTIFACT_ID_2 + "-dependent-jar", false);
+
+        setUpstreamProjects(dependentModuleMock, changedModuleMock, mavenProjectMock);
+        setDownstreamProjects(changedModuleMock, dependentModuleMock);
+        setDownstreamProjects(mavenProjectMock, changedModuleMock, dependentModuleMock);   // just for consistency
+
+        projectProperties.put(Property.buildDownstream.fullName(), "false");
+        projectProperties.put(Property.buildAll.fullName(), "true");
+        projectProperties.put(Property.argsForUpstreamModules.fullName(), "foo=bar");
+
+        underTest.act();
+
+        verify(mavenSessionMock, never()).setProjects(anyListOf(MavenProject.class));
+
+        assertProjectPropertiesEqual(mavenProjectMock, ImmutableMap.of("foo", "bar"));
+        assertProjectPropertiesEqual(changedModuleMock, Collections.emptyMap());
+        assertProjectPropertiesEqual(dependentModuleMock, Collections.emptyMap());
     }
 
     @Test
@@ -144,7 +351,7 @@ public class UnchangedProjectsRemoverTest {
 
         underTest.act();
 
-        Mockito.verify(mavenSessionMock).setProjects(Arrays.asList(mavenProjectMock, changedModuleMock));
+        verify(mavenSessionMock).setProjects(Arrays.asList(mavenProjectMock, changedModuleMock));
     }
 
     @Test
@@ -157,7 +364,7 @@ public class UnchangedProjectsRemoverTest {
 
         underTest.act();
 
-        Mockito.verify(mavenSessionMock).setProjects(
+        verify(mavenSessionMock).setProjects(
                 Arrays.asList(mavenProjectMock, unchangedModuleMock, changedModuleMock));
     }
 
@@ -170,7 +377,7 @@ public class UnchangedProjectsRemoverTest {
 
         underTest.act();
 
-        Mockito.verify(mavenSessionMock).setProjects(
+        verify(mavenSessionMock).setProjects(
                 Arrays.asList(mavenProjectMock, unchangedModuleMock, changedModuleMock));
     }
 
@@ -183,7 +390,7 @@ public class UnchangedProjectsRemoverTest {
 
         underTest.act();
 
-        Mockito.verify(mavenSessionMock).setProjects(
+        verify(mavenSessionMock).setProjects(
                 Arrays.asList(mavenProjectMock, unchangedModuleMock, changedModuleMock));
     }
 
@@ -192,14 +399,13 @@ public class UnchangedProjectsRemoverTest {
         MavenProject changedProjectMock = addModuleMock(ARTIFACT_ID_2, true);
         MavenProject dependentWar = addModuleMock(ARTIFACT_ID_2_DEP_WAR, false, "war");
 
-        when(projectDependencyGraphMock.getDownstreamProjects(changedProjectMock, true))
-                .thenReturn(Arrays.asList(dependentWar));
+        setDownstreamProjects(changedProjectMock, dependentWar);
 
         projectProperties.put(Property.excludeTransitiveModulesPackagedAs.fullName(), "war");
 
         underTest.act();
 
-        Mockito.verify(mavenSessionMock).setProjects(
+        verify(mavenSessionMock).setProjects(
                 Arrays.asList(changedProjectMock));
     }
 
@@ -209,14 +415,13 @@ public class UnchangedProjectsRemoverTest {
         MavenProject dependentWar = addModuleMock(ARTIFACT_ID_2_DEP_WAR, false, "war");
         MavenProject dependentJar = addModuleMock(ARTIFACT_ID_2 + "-dependent-jar", false);
 
-        when(projectDependencyGraphMock.getDownstreamProjects(changedProjectMock, true))
-                .thenReturn(Arrays.asList(dependentWar, dependentJar));
+        setDownstreamProjects(changedProjectMock, dependentWar, dependentJar);
 
         projectProperties.put(Property.excludeTransitiveModulesPackagedAs.fullName(), "war");
 
         underTest.act();
 
-        Mockito.verify(mavenSessionMock).setProjects(
+        verify(mavenSessionMock).setProjects(
                 Arrays.asList(changedProjectMock, dependentJar));
     }
 
@@ -226,14 +431,13 @@ public class UnchangedProjectsRemoverTest {
         MavenProject dependentWar = addModuleMock(ARTIFACT_ID_2_DEP_WAR, false, "war");
         MavenProject dependentEar = addModuleMock(ARTIFACT_ID_2 + "-dependent-ear", false, "ear");
 
-        when(projectDependencyGraphMock.getDownstreamProjects(changedProjectMock, true))
-                .thenReturn(Arrays.asList(dependentWar, dependentEar));
+        setDownstreamProjects(changedProjectMock, dependentWar, dependentEar);
 
         projectProperties.put(Property.excludeTransitiveModulesPackagedAs.fullName(), "war,ear");
 
         underTest.act();
 
-        Mockito.verify(mavenSessionMock).setProjects(
+        verify(mavenSessionMock).setProjects(
                 Arrays.asList(changedProjectMock));
     }
 
@@ -242,15 +446,14 @@ public class UnchangedProjectsRemoverTest {
         MavenProject changedProjectMock = addModuleMock(ARTIFACT_ID_2, true);
         MavenProject dependentWar = addModuleMock(ARTIFACT_ID_2_DEP_WAR, false, "war");
 
-        when(projectDependencyGraphMock.getDownstreamProjects(changedProjectMock, true))
-                .thenReturn(Arrays.asList(dependentWar));
+        setDownstreamProjects(changedProjectMock, dependentWar);
 
         projectProperties.put(Property.excludeTransitiveModulesPackagedAs.fullName(), "war");
         projectProperties.put(Property.buildAll.fullName(), "true");
 
         underTest.act();
 
-        Mockito.verify(mavenSessionMock, never())
+        verify(mavenSessionMock, never())
                 .setProjects(anyListOf(MavenProject.class));
     }
 
@@ -259,15 +462,14 @@ public class UnchangedProjectsRemoverTest {
         MavenProject changedProjectMock = addModuleMock(ARTIFACT_ID_2, true);
         MavenProject dependentWar = addModuleMock(ARTIFACT_ID_2_DEP_WAR, false, "war");
 
-        when(projectDependencyGraphMock.getDownstreamProjects(changedProjectMock, true))
-                .thenReturn(Arrays.asList(dependentWar));
+        setDownstreamProjects(changedProjectMock, dependentWar);
 
         projectProperties.put(Property.excludeTransitiveModulesPackagedAs.fullName(), "war");
         projectProperties.put(Property.forceBuildModules.fullName(), dependentWar.getArtifactId());
 
         underTest.act();
 
-        Mockito.verify(mavenSessionMock).setProjects(
+        verify(mavenSessionMock).setProjects(
                 Arrays.asList(dependentWar, changedProjectMock));
     }
 
@@ -282,7 +484,7 @@ public class UnchangedProjectsRemoverTest {
 
         underTest.act();
 
-        Mockito.verify(mavenSessionMock).setProjects(
+        verify(mavenSessionMock).setProjects(
                 Arrays.asList(changedProjectMock, dependentWar));
     }
 
@@ -292,14 +494,13 @@ public class UnchangedProjectsRemoverTest {
         MavenProject dependentWar = addModuleMock(ARTIFACT_ID_2_DEP_WAR, true, "war");
 
         // war module is changed, must be retained - even if depending on changedProjectMock!
-        when(projectDependencyGraphMock.getDownstreamProjects(changedProjectMock, true))
-                .thenReturn(Arrays.asList(dependentWar));
+        setDownstreamProjects(changedProjectMock, dependentWar);
 
         projectProperties.put(Property.excludeTransitiveModulesPackagedAs.fullName(), "war");
 
         underTest.act();
 
-        Mockito.verify(mavenSessionMock).setProjects(
+        verify(mavenSessionMock).setProjects(
                 Arrays.asList(changedProjectMock, dependentWar));
     }
 
@@ -318,8 +519,35 @@ public class UnchangedProjectsRemoverTest {
 
         when(changedModuleMock.getProperties()).thenReturn(new Properties());
 
-        when(projectDependencyGraphMock.getUpstreamProjects(changedModuleMock, true))
-                .thenReturn(Collections.singletonList(mavenProjectMock));
+        setUpstreamProjects(changedModuleMock, mavenProjectMock);
         return changedModuleMock;
     }
+
+    private void setUpstreamProjects(MavenProject module, MavenProject... upstreamModules) {
+        when(projectDependencyGraphMock.getUpstreamProjects(module, true)).thenReturn(Arrays.asList(upstreamModules));
+    }
+
+    private void setDownstreamProjects(MavenProject module, MavenProject... downstreamModules) {
+        when(projectDependencyGraphMock.getDownstreamProjects(module, true)).thenReturn(Arrays.asList(downstreamModules));
+    }
+
+    private void assertProjectPropertiesEqual(MavenProject project, Map<String, String> expected) {
+        TreeMap<String, String> actual = project.getProperties().entrySet().stream()
+                .filter(e -> !e.getKey().toString().startsWith(Property.PREFIX))    // we don't want to check for GIB properties here!
+                .collect(Collectors.toMap(
+                        e -> e.getKey().toString(),
+                        e -> e.getValue().toString(),
+                        (a, b) -> a,
+                        TreeMap::new));
+        assertEquals("Unexpected project properties of " + project, new TreeMap<>(expected), actual);
+    }
+
+    private void setUpAndDownstreamsForBuildUpstreamModeTests(MavenProject changedModuleMock, MavenProject unchangedModuleMock,
+        MavenProject dependsOnBothModuleMock) {
+        // dependsOnBothModuleMock directly depends on both changedModuleMock & unchangedModuleMock + transitively on mavenProjectMock
+        setUpstreamProjects(dependsOnBothModuleMock, changedModuleMock, unchangedModuleMock, mavenProjectMock);
+        setDownstreamProjects(changedModuleMock, dependsOnBothModuleMock);
+        setDownstreamProjects(unchangedModuleMock, dependsOnBothModuleMock);
+        setDownstreamProjects(mavenProjectMock, changedModuleMock, unchangedModuleMock, dependsOnBothModuleMock);   // just for consistency
+    }
 }