Repository: JonathonReinhart/scuba
Pull request #128 (2019-03-22 13:32:34)

Add support for nested scripts
This enables concatenation of scripts from multiple external yaml blocks, as suggested in #127.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 90668f3..82bc32d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 ## [Unreleased]
 ### Added
 - Enable scuba to override entrypoint via `--entrypoint` or `.scuba.yml` (#125)
+- Add support for nested scripts (#128)
 
 ### Changed
 - Don't run image entrypoint for each line in a mult-line alias (#121)
diff --git a/doc/yaml-reference.md b/doc/yaml-reference.md
index 996f554..6d066d3 100644
--- a/doc/yaml-reference.md
+++ b/doc/yaml-reference.md
@@ -165,8 +165,92 @@ image: gcc:5.1
 image: !from_yaml .gitlab-ci.yml image
 ```
 
+Here's a more elaborate example which defines multiple aliases which correspond
+to jobs defined by `.gitlab-ci.yml`:
+
+**`.gitlab-ci.yml`**
+```yaml
+
+build_c:
+  image: gcc:5.1
+  script:
+    - make something
+    - make something-else
+
+build_py:
+  image: python:3.7
+  script:
+    - setup.py bdist_wheel
+```
+
+**`.scuba.yml`**
+```yaml
+image: not-specified (see issue #124)
+
+aliases
+  build_c:
+    image: !from_yaml .gitlab-ci.yml build_c.image
+    script: !from_yaml .gitlab-ci.yml build_c.script
+  build_py:
+    image: !from_yaml .gitlab-ci.yml build_py.image
+    script: !from_yaml .gitlab-ci.yml build_py.script
+```
+
+An easier but less-flexible method is to simply import the entire job's
+definition. This works becaue Scuba ignores unrecognized keys in an `alias`:
+
+**`.gitlab-ci.yml`**
+```yaml
+
+build_c:
+  image: gcc:5.1
+  script:
+    - make something
+    - make something-else
+
+build_py:
+  image: python:3.7
+  script:
+    - setup.py bdist_wheel
+```
+
+**`.scuba.yml`**
+```yaml
+image: not-specified (see issue #124)
+
+aliases
+  build_c: !from_yaml .gitlab-ci.yml build_c
+  build_py: !from_yaml .gitlab-ci.yml build_py
+```
+
+This example which concatenates two jobs from `.gitlab-ci.yml` into a
+single alias. This works by flattening the effective `script` node that results
+by including two elements that are lists.
+
+**`.gitlab-ci.yml`**
+```yaml
+image: gcc:5.1
+
+part1:
+  script:
+    - make something
+part2:
+  script:
+    - make something-else
+```
+
+**`.scuba.yml`**
+```yaml
+image: !from_yaml .gitlab-ci.yml image
+
+aliases
+  all_parts:
+    script:
+      - !from_yaml .gitlab-ci.yml part1.script
+      - !from_yaml .gitlab-ci.yml part2.script
+```
 
-[YAML]: http://yaml.org/
-[`.gitlab-ci.yml`]: http://doc.gitlab.com/ce/ci/yaml/README.html
 
 
+[YAML]: http://yaml.org/
+[`.gitlab-ci.yml`]: http://doc.gitlab.com/ce/ci/yaml/README.html
diff --git a/example/external_yaml_concatenate/.other.yml b/example/external_yaml_concatenate/.other.yml
new file mode 100644
index 0000000..821b808
--- /dev/null
+++ b/example/external_yaml_concatenate/.other.yml
@@ -0,0 +1,15 @@
+image: busybox 
+
+part1:
+  script:
+    - echo "Hello"
+    - echo "darkness"
+
+part2:
+  script:
+    - echo "my"
+    - echo "old"
+
+part3:
+  script:
+    - echo "friend"
diff --git a/example/external_yaml_concatenate/.scuba.yml b/example/external_yaml_concatenate/.scuba.yml
new file mode 100644
index 0000000..c2e1aaf
--- /dev/null
+++ b/example/external_yaml_concatenate/.scuba.yml
@@ -0,0 +1,8 @@
+image: !from_yaml .other.yml image
+
+aliases:
+  allparts:
+    script:
+      - !from_yaml .other.yml part1.script
+      - !from_yaml .other.yml part2.script
+      - !from_yaml .other.yml part3.script
diff --git a/example/external_yaml_concatenate/run_example.sh b/example/external_yaml_concatenate/run_example.sh
new file mode 100755
index 0000000..597bfcf
--- /dev/null
+++ b/example/external_yaml_concatenate/run_example.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+cd $(dirname $0)
+
+scuba allparts
diff --git a/example/run_all.sh b/example/run_all.sh
index e4d84f7..826b0d2 100755
--- a/example/run_all.sh
+++ b/example/run_all.sh
@@ -6,6 +6,7 @@ cd $(dirname $0)
 ./03_search_up/one/two/three/four/run_example.sh
 ./external_yaml_simple/run_example.sh
 ./external_yaml_nested/run_example.sh
+./external_yaml_concatenate/run_example.sh
 ./scubainit_hooks/run_example.sh
 ./alias_multiline/run_example.sh
 ./per_alias_image/run_example.sh
diff --git a/scuba/config.py b/scuba/config.py
index 01570e9..f9f672d 100644
--- a/scuba/config.py
+++ b/scuba/config.py
@@ -319,6 +319,8 @@ class ScubaConfig(object):
                     command.pop(0)
                     result.script = [alias.script[0] + ' ' + shell_quote_cmd(command)]
 
+            result.script = flatten_list(result.script)
+
         return result
 
 
diff --git a/scuba/utils.py b/scuba/utils.py
index 58b3828..a9226f4 100644
--- a/scuba/utils.py
+++ b/scuba/utils.py
@@ -67,3 +67,16 @@ def parse_env_var(s):
 
     k = parts[0]
     return (k, os.getenv(k, ''))
+
+
+def flatten_list(x):
+    if not isinstance(x, list):
+        raise ValueError("argument is not a list")
+    result = []
+    for i in x:
+        if isinstance(i, list):
+            for j in flatten_list(i):
+                result.append(j)
+        else:
+            result.append(i)
+    return result


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
index 44aa173..82a347a 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -358,6 +358,27 @@ class TestMain(TmpDirTestCase):
         assert_str_equalish(out, data)
 
 
+    def test_nested_sript(self):
+        '''Verify nested scripts works'''
+        with open('.scuba.yml', 'w') as f:
+            f.write('image: {}\n'.format(DOCKER_IMAGE))
+            f.write('aliases:\n')
+            f.write('  foo:\n')
+            f.write('    script:\n')
+            f.write('      - echo "This"\n')
+            f.write('      - - echo "list"\n')
+            f.write('        - echo "is"\n')
+            f.write('        - echo "nested"\n')
+            f.write('        - - echo "kinda"\n')
+            f.write('          - echo "crazy"\n')
+
+        test_str = 'This list is nested kinda crazy'
+        out, _ = self.run_scuba(['foo'])
+
+        out = out.replace('\n', ' ')
+        assert_str_equalish(out, test_str)
+
+
     ############################################################################
     # Entrypoint
 
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 02c3aaf..5a99f9c 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -98,3 +98,32 @@ class TestUtils(TestCase):
         with mocked_os_env():
             result = scuba.utils.parse_env_var('NOTSET')
         self.assertEqual(result, ('NOTSET', ''))
+
+
+    def test_flatten_list__not_nested(self):
+        sample = [1, 2, 3, 4]
+        result = scuba.utils.flatten_list(sample)
+        self.assertEqual(result, sample)
+
+    def test_flatten_list__nested_1(self):
+        sample = [
+            1,
+            [2, 3],
+            4,
+            [5, 6, 7],
+        ]
+        exp = range(1, 7+1)
+        result = scuba.utils.flatten_list(sample)
+        assert_seq_equal(result, exp)
+
+    def test_flatten_list__nested_many(self):
+        sample = [
+            1,
+            [2, 3],
+            [4, 5, [6, 7, 8]],
+            9, 10,
+            [11, [12, [13, [14, [15, [16, 17, 18]]]]]],
+        ]
+        exp = range(1, 18+1)
+        result = scuba.utils.flatten_list(sample)
+        assert_seq_equal(result, exp)