diff --git a/recipes/README.recipes.md b/recipes/README.recipes.md
index a16b5444ab..9a3aa7d193 100644
--- a/recipes/README.recipes.md
+++ b/recipes/README.recipes.md
@@ -13,6 +13,7 @@
* [gitiles](#recipe_modules-gitiles)
* [gsutil](#recipe_modules-gsutil)
* [infra_paths](#recipe_modules-infra_paths)
+ * [luci_migration](#recipe_modules-luci_migration)
* [presubmit](#recipe_modules-presubmit)
* [rietveld](#recipe_modules-rietveld)
* [tryserver](#recipe_modules-tryserver)
@@ -31,6 +32,7 @@
* [gitiles:examples/full](#recipes-gitiles_examples_full)
* [gsutil:examples/full](#recipes-gsutil_examples_full)
* [infra_paths:examples/full](#recipes-infra_paths_examples_full)
+ * [luci_migration:tests/full](#recipes-luci_migration_tests_full)
* [presubmit:examples/full](#recipes-presubmit_examples_full)
* [rietveld:examples/full](#recipes-rietveld_examples_full)
* [tryserver:examples/full](#recipes-tryserver_examples_full)
@@ -612,6 +614,30 @@ It returns git_cache path if it is defined (Buildbot world), otherwise
uses the more generic [CACHE]/git path (LUCI world).
— **def [initialize](/recipes/recipe_modules/infra_paths/api.py#11)(self):**
+### *recipe_modules* / [luci\_migration](/recipes/recipe_modules/luci_migration)
+
+[DEPS](/recipes/recipe_modules/luci_migration/__init__.py#5): [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/properties][recipe_engine/recipe_modules/properties]
+
+#### **class [LuciMigrationApi](/recipes/recipe_modules/luci_migration/api.py#8)([RecipeApi][recipe_engine/wkt/RecipeApi]):**
+
+This module assists in migrating builders from Buildbot to pure LUCI stack.
+
+Finishing migration means you no longer depend on this module.
+
+ **@property**
— **def [is\_luci](/recipes/recipe_modules/luci_migration/api.py#18)(self):**
+
+True if runs on LUCI stack.
+
+ **@property**
— **def [is\_prod](/recipes/recipe_modules/luci_migration/api.py#23)(self):**
+
+True if this builder is in production.
+
+Typical usage is to modify steps which produce external side-effects so that
+non-production runs of the recipe do not affect production data.
+
+Examples:
+ * Uploading to an alternate google storage file name when in non-prod mode
+ * Appending a 'non-production' tag to external RPCs
### *recipe_modules* / [presubmit](/recipes/recipe_modules/presubmit)
[DEPS](/recipes/recipe_modules/presubmit/__init__.py#1): [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/python][recipe_engine/recipe_modules/python], [recipe\_engine/step][recipe_engine/recipe_modules/step]
@@ -817,6 +843,11 @@ Move things around in a loop!
[DEPS](/recipes/recipe_modules/infra_paths/examples/full.py#7): [infra\_paths](#recipe_modules-infra_paths), [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/platform][recipe_engine/recipe_modules/platform], [recipe\_engine/properties][recipe_engine/recipe_modules/properties], [recipe\_engine/step][recipe_engine/recipe_modules/step]
— **def [RunSteps](/recipes/recipe_modules/infra_paths/examples/full.py#16)(api):**
+### *recipes* / [luci\_migration:tests/full](/recipes/recipe_modules/luci_migration/tests/full.py)
+
+[DEPS](/recipes/recipe_modules/luci_migration/tests/full.py#7): [luci\_migration](#recipe_modules-luci_migration), [recipe\_engine/step][recipe_engine/recipe_modules/step]
+
+— **def [RunSteps](/recipes/recipe_modules/luci_migration/tests/full.py#13)(api):**
### *recipes* / [presubmit:examples/full](/recipes/recipe_modules/presubmit/examples/full.py)
[DEPS](/recipes/recipe_modules/presubmit/examples/full.py#5): [presubmit](#recipe_modules-presubmit)
diff --git a/recipes/recipe_modules/luci_migration/__init__.py b/recipes/recipe_modules/luci_migration/__init__.py
new file mode 100644
index 0000000000..23fb5ff5da
--- /dev/null
+++ b/recipes/recipe_modules/luci_migration/__init__.py
@@ -0,0 +1,25 @@
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+DEPS = [
+ 'recipe_engine/path',
+ 'recipe_engine/properties',
+]
+
+from recipe_engine.recipe_api import Property
+from recipe_engine.config import ConfigGroup, Single
+
+PROPERTIES = {
+ '$depot_tools/luci_migration': Property(
+ help='Properties specifically for the luci_migration module',
+ param_name='migration_properties',
+ kind=ConfigGroup(
+ # Whether builder runs on LUCI stack.
+ is_luci=Single(bool),
+ # Whether builder is in production.
+ is_prod=Single(bool),
+ ),
+ default={},
+ ),
+}
diff --git a/recipes/recipe_modules/luci_migration/api.py b/recipes/recipe_modules/luci_migration/api.py
new file mode 100644
index 0000000000..92fd3981a3
--- /dev/null
+++ b/recipes/recipe_modules/luci_migration/api.py
@@ -0,0 +1,34 @@
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from recipe_engine import recipe_api
+
+
+class LuciMigrationApi(recipe_api.RecipeApi):
+ """This module assists in migrating builders from Buildbot to pure LUCI stack.
+
+ Finishing migration means you no longer depend on this module.
+ """
+
+ def __init__(self, migration_properties, **kwargs):
+ super(LuciMigrationApi, self).__init__(**kwargs)
+ self._migration_properties = migration_properties
+
+ @property
+ def is_luci(self):
+ """True if this recipe is currently running on LUCI stack."""
+ return bool(self._migration_properties.get('is_luci', False))
+
+ @property
+ def is_prod(self):
+ """True if this recipe is currently running in production.
+
+ Typical usage is to modify steps which produce external side-effects so that
+ non-production runs of the recipe do not affect production data.
+
+ Examples:
+ * Uploading to an alternate google storage file name when in non-prod mode
+ * Appending a 'non-production' tag to external RPCs
+ """
+ return bool(self._migration_properties.get('is_prod', True))
diff --git a/recipes/recipe_modules/luci_migration/test_api.py b/recipes/recipe_modules/luci_migration/test_api.py
new file mode 100644
index 0000000000..0655405367
--- /dev/null
+++ b/recipes/recipe_modules/luci_migration/test_api.py
@@ -0,0 +1,20 @@
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from recipe_engine import recipe_test_api
+
+
+class LuciMigrationTestApi(recipe_test_api.RecipeTestApi):
+ def __call__(self, is_luci, is_prod):
+ """Simulate luci migration state of a builder."""
+ assert isinstance(is_luci, bool), '%r (%s)' % (is_luci, type(is_luci))
+ assert isinstance(is_prod, bool), '%r (%s)' % (is_prod, type(is_prod))
+ ret = self.test(None)
+ ret.properties = {
+ '$depot_tools/luci_migration': {
+ 'is_luci': is_luci,
+ 'is_prod': is_prod,
+ },
+ }
+ return ret
diff --git a/recipes/recipe_modules/luci_migration/tests/full.expected/basic.json b/recipes/recipe_modules/luci_migration/tests/full.expected/basic.json
new file mode 100644
index 0000000000..ad81ee21a1
--- /dev/null
+++ b/recipes/recipe_modules/luci_migration/tests/full.expected/basic.json
@@ -0,0 +1,16 @@
+[
+ {
+ "cmd": [],
+ "name": "show properties",
+ "~followup_annotations": [
+ "@@@STEP_LOG_LINE@result@is_luci: True@@@",
+ "@@@STEP_LOG_LINE@result@is_prod: False@@@",
+ "@@@STEP_LOG_END@result@@@"
+ ]
+ },
+ {
+ "name": "$result",
+ "recipe_result": null,
+ "status_code": 0
+ }
+]
\ No newline at end of file
diff --git a/recipes/recipe_modules/luci_migration/tests/full.py b/recipes/recipe_modules/luci_migration/tests/full.py
new file mode 100644
index 0000000000..5b177e1431
--- /dev/null
+++ b/recipes/recipe_modules/luci_migration/tests/full.py
@@ -0,0 +1,22 @@
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import json
+
+DEPS = [
+ 'depot_tools/luci_migration',
+ 'recipe_engine/step',
+]
+
+
+def RunSteps(api):
+ api.step('show properties', [])
+ api.step.active_result.presentation.logs['result'] = [
+ 'is_luci: %r' % (api.luci_migration.is_luci,),
+ 'is_prod: %r' % (api.luci_migration.is_prod,),
+ ]
+
+
+def GenTests(api):
+ yield api.test('basic') + api.luci_migration(is_luci=True, is_prod=False)