submodule_roll: Return a list of Roll objects

Also, change the API so only one submodule is processed at a time.

Bug: b/341756093
Change-Id: I545f94164dc74c8fdbe851f1d69dc1ad59b9a41b
Reviewed-on: https://pigweed-review.googlesource.com/c/infra/recipes/+/230833
Reviewed-by: Oliver Newman <olivernewman@google.com>
Pigweed-Auto-Submit: Rob Mohr <mohrr@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed-service-accounts.iam.gserviceaccount.com>
Lint: Lint 🤖 <android-build-ayeaye@system.gserviceaccount.com>
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/recipe_modules/submodule_roll/api.py b/recipe_modules/submodule_roll/api.py
index 9c0da26..9d6aa85 100644
--- a/recipe_modules/submodule_roll/api.py
+++ b/recipe_modules/submodule_roll/api.py
@@ -16,6 +16,7 @@
 
 import configparser
 import dataclasses
+import functools
 import io
 import re
 from typing import TYPE_CHECKING
@@ -78,93 +79,93 @@
 
         return RevisionChange(old=old_revision, new=new_revision)
 
-    def update(
-        self,
-        checkout: checkout_api.CheckoutContext,
-        submodule_entries: Sequence[SubmoduleEntry],
-    ) -> dict[config_types.Path, roll_util_api.Roll]:
+    @functools.cache
+    def read_gitmodules(self, path):
         # Confirm the given path is actually a submodule.
-        gitmodules = self.m.file.read_text(
-            'read .gitmodules', checkout.root / '.gitmodules'
-        )
+        gitmodules = self.m.file.read_text('read .gitmodules', path)
         # Example .gitmodules file:
         # [submodule "third_party/pigweed"]
         #   path = third_party/pigweed
         #   url = https://pigweed.googlesource.com/pigweed/pigweed
 
-        # configparser doesn't like leading whitespace on lines, despite what its
-        # documentation says.
+        # configparser doesn't like leading whitespace on lines, despite what
+        # its documentation says.
         gitmodules = re.sub(r'\n\s+', '\n', gitmodules)
         parser = configparser.RawConfigParser()
         parser.readfp(io.StringIO(gitmodules))
+        return parser
 
-        rolls: dict[config_types.Path, self.m.roll_util.Roll] = {}
+    def update(
+        self,
+        checkout: checkout_api.CheckoutContext,
+        submodule_entry: SubmoduleEntry,
+    ) -> roll_util_api.Roll:
+        gitmodules = self.read_gitmodules(checkout.root / '.gitmodules')
 
-        for entry in submodule_entries:
-            submodule = Submodule(
-                path=entry.path,
-                name=entry.name or entry.path,
-                branch=entry.branch,
+        submodule = Submodule(
+            path=submodule_entry.path,
+            name=submodule_entry.name or submodule_entry.path,
+            branch=submodule_entry.branch,
+        )
+        submodule.dir = checkout.root / submodule.path
+
+        with self.m.step.nest(submodule.name) as pres:
+            section = f'submodule "{submodule.name}"'
+            if not gitmodules.has_section(section):
+                sections = gitmodules.sections()
+                submodules = sorted(
+                    re.sub(r'^.*"(.*)"$', r'\1', x) for x in sections
+                )
+                raise self.m.step.StepFailure(
+                    'no submodule "{}" (submodules: {})'.format(
+                        submodule.name,
+                        ', '.join('"{}"'.format(x) for x in submodules),
+                    )
+                )
+
+            if not submodule.branch:
+                try:
+                    submodule.branch = gitmodules.get(section, 'branch')
+                except configparser.NoOptionError:
+                    submodule.branch = 'main'
+
+            submodule.remote = self.m.roll_util.normalize_remote(
+                gitmodules.get(section, 'url'),
+                checkout.options.remote,
             )
-            submodule.dir = checkout.root / submodule.path
 
-            with self.m.step.nest(submodule.name) as pres:
-                section = f'submodule "{submodule.name}"'
-                if not parser.has_section(section):
-                    sections = parser.sections()
-                    submodules = sorted(
-                        re.sub(r'^.*"(.*)"$', r'\1', x) for x in sections
-                    )
-                    raise self.m.step.StepFailure(
-                        'no submodule "{}" (submodules: {})'.format(
-                            submodule.name,
-                            ', '.join('"{}"'.format(x) for x in submodules),
-                        )
-                    )
+            new_revision = self.m.git_roll_util.resolve_new_revision(
+                submodule.remote,
+                submodule.branch,
+                checkout.remotes_equivalent,
+            )
 
-                if not submodule.branch:
-                    try:
-                        submodule.branch = parser.get(section, 'branch')
-                    except configparser.NoOptionError:
-                        submodule.branch = 'main'
+            change = self.update_pin(
+                checkout,
+                submodule.dir,
+                new_revision,
+            )
 
-                submodule.remote = self.m.roll_util.normalize_remote(
-                    parser.get(section, 'url'),
-                    checkout.options.remote,
+            direction = self.m.roll_util.get_roll_direction(
+                submodule.dir, change.old, change.new
+            )
+
+            # If the primary roll is not necessary or is backwards we can
+            # exit immediately and don't need to check deps.
+            if not self.m.roll_util.can_roll(direction):
+                pres.step_summary_text = 'no roll required'
+                self.m.roll_util.skip_roll_step(
+                    submodule.remote, change.old, change.new
                 )
+                return []
 
-                new_revision = self.m.git_roll_util.resolve_new_revision(
-                    submodule.remote,
-                    submodule.branch,
-                    checkout.remotes_equivalent,
+            return [
+                self.m.roll_util.create_roll(
+                    project_name=str(submodule.path),
+                    old_revision=change.old,
+                    new_revision=change.new,
+                    proj_dir=submodule.dir,
+                    direction=direction,
+                    nest_steps=False,
                 )
-
-                change = self.update_pin(
-                    checkout,
-                    submodule.dir,
-                    new_revision,
-                )
-
-                direction = self.m.roll_util.get_roll_direction(
-                    submodule.dir, change.old, change.new
-                )
-
-                # If the primary roll is not necessary or is backwards we can
-                # exit immediately and don't need to check deps.
-                if self.m.roll_util.can_roll(direction):
-                    rolls[submodule.path] = self.m.roll_util.create_roll(
-                        project_name=str(submodule.path),
-                        old_revision=change.old,
-                        new_revision=change.new,
-                        proj_dir=submodule.dir,
-                        direction=direction,
-                        nest_steps=False,
-                    )
-
-                else:
-                    pres.step_summary_text = 'no roll required'
-                    self.m.roll_util.skip_roll_step(
-                        submodule.remote, change.old, change.new
-                    )
-
-        return rolls
+            ]
diff --git a/recipe_modules/submodule_roll/tests/full.expected/missing.json b/recipe_modules/submodule_roll/tests/full.expected/missing.json
index 807f498..75a27e8 100644
--- a/recipe_modules/submodule_roll/tests/full.expected/missing.json
+++ b/recipe_modules/submodule_roll/tests/full.expected/missing.json
@@ -21,157 +21,6 @@
   },
   {
     "cmd": [],
-    "name": "a1",
-    "~followup_annotations": [
-      "@@@STEP_SUMMARY_TEXT@no roll required@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "ls-remote",
-      "--heads",
-      "https://foo.googlesource.com/a1",
-      "main"
-    ],
-    "name": "a1.git ls-remote",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_LOG_LINE@stdout@h3ll0\trefs/heads/main@@@",
-      "@@@STEP_LOG_END@stdout@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "submodule",
-      "update",
-      "--init",
-      "--jobs",
-      "4",
-      "[START_DIR]/checkout/a1"
-    ],
-    "cwd": "[START_DIR]/checkout",
-    "name": "a1.git submodule update",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "log",
-      "--max-count=1",
-      "--pretty=format:%H"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.get old revision",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@1111111111111111111111111111111111111111@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "fetch",
-      "origin",
-      "h3ll0"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.git fetch",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "checkout",
-      "FETCH_HEAD"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.git checkout",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "log",
-      "--max-count=1",
-      "--pretty=format:%H"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.get new revision",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@2222222222222222222222222222222222222222@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "a1.get roll direction",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "merge-base",
-      "--is-ancestor",
-      "1111111111111111111111111111111111111111",
-      "2222222222222222222222222222222222222222"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.get roll direction.is forward",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "merge-base",
-      "--is-ancestor",
-      "2222222222222222222222222222222222222222",
-      "1111111111111111111111111111111111111111"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.get roll direction.is backward",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "a1.get roll direction.get roll direction",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@up-to-date@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "a1.cancelling roll",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@not updating from 1111111 to 2222222 because 1111111 is newer than 2222222@@@",
-      "@@@STEP_LINK@1111111111111111111111111111111111111111@https://foo.googlesource.com/a1/+/1111111111111111111111111111111111111111@@@",
-      "@@@STEP_LINK@2222222222222222222222222222222222222222@https://foo.googlesource.com/a1/+/2222222222222222222222222222222222222222@@@"
-    ]
-  },
-  {
-    "cmd": [],
     "name": "b2",
     "~followup_annotations": [
       "@@@STEP_FAILURE@@@"
diff --git a/recipe_modules/submodule_roll/tests/full.expected/noop.json b/recipe_modules/submodule_roll/tests/full.expected/noop.json
index 006b9dd..d6af537 100644
--- a/recipe_modules/submodule_roll/tests/full.expected/noop.json
+++ b/recipe_modules/submodule_roll/tests/full.expected/noop.json
@@ -13,9 +13,6 @@
     "infra_step": true,
     "name": "read .gitmodules",
     "~followup_annotations": [
-      "@@@STEP_LOG_LINE@.gitmodules@[submodule \"a1\"]@@@",
-      "@@@STEP_LOG_LINE@.gitmodules@\tpath = a1@@@",
-      "@@@STEP_LOG_LINE@.gitmodules@\turl = https://foo.googlesource.com/a1@@@",
       "@@@STEP_LOG_LINE@.gitmodules@[submodule \"b2\"]@@@",
       "@@@STEP_LOG_LINE@.gitmodules@\tpath = b2@@@",
       "@@@STEP_LOG_LINE@.gitmodules@\turl = https://foo.googlesource.com/b2@@@",
@@ -25,157 +22,6 @@
   },
   {
     "cmd": [],
-    "name": "a1",
-    "~followup_annotations": [
-      "@@@STEP_SUMMARY_TEXT@no roll required@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "ls-remote",
-      "--heads",
-      "https://foo.googlesource.com/a1",
-      "main"
-    ],
-    "name": "a1.git ls-remote",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_LOG_LINE@stdout@h3ll0\trefs/heads/main@@@",
-      "@@@STEP_LOG_END@stdout@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "submodule",
-      "update",
-      "--init",
-      "--jobs",
-      "4",
-      "[START_DIR]/checkout/a1"
-    ],
-    "cwd": "[START_DIR]/checkout",
-    "name": "a1.git submodule update",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "log",
-      "--max-count=1",
-      "--pretty=format:%H"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.get old revision",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@1111111111111111111111111111111111111111@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "fetch",
-      "origin",
-      "h3ll0"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.git fetch",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "checkout",
-      "FETCH_HEAD"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.git checkout",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "log",
-      "--max-count=1",
-      "--pretty=format:%H"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.get new revision",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@2222222222222222222222222222222222222222@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "a1.get roll direction",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "merge-base",
-      "--is-ancestor",
-      "1111111111111111111111111111111111111111",
-      "2222222222222222222222222222222222222222"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.get roll direction.is forward",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "merge-base",
-      "--is-ancestor",
-      "2222222222222222222222222222222222222222",
-      "1111111111111111111111111111111111111111"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.get roll direction.is backward",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "a1.get roll direction.get roll direction",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@up-to-date@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "a1.cancelling roll",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@not updating from 1111111 to 2222222 because 1111111 is newer than 2222222@@@",
-      "@@@STEP_LINK@1111111111111111111111111111111111111111@https://foo.googlesource.com/a1/+/1111111111111111111111111111111111111111@@@",
-      "@@@STEP_LINK@2222222222222222222222222222222222222222@https://foo.googlesource.com/a1/+/2222222222222222222222222222222222222222@@@"
-    ]
-  },
-  {
-    "cmd": [],
     "name": "b2",
     "~followup_annotations": [
       "@@@STEP_SUMMARY_TEXT@no roll required@@@"
diff --git a/recipe_modules/submodule_roll/tests/full.expected/partial_noop.json b/recipe_modules/submodule_roll/tests/full.expected/partial_noop.json
deleted file mode 100644
index 556c735..0000000
--- a/recipe_modules/submodule_roll/tests/full.expected/partial_noop.json
+++ /dev/null
@@ -1,527 +0,0 @@
-[
-  {
-    "cmd": [
-      "vpython3",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "copy",
-      "[START_DIR]/checkout/.gitmodules",
-      "/path/to/tmp/"
-    ],
-    "infra_step": true,
-    "name": "read .gitmodules",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@.gitmodules@[submodule \"a1\"]@@@",
-      "@@@STEP_LOG_LINE@.gitmodules@\tpath = a1@@@",
-      "@@@STEP_LOG_LINE@.gitmodules@\turl = sso://foo/a1@@@",
-      "@@@STEP_LOG_LINE@.gitmodules@[submodule \"b2\"]@@@",
-      "@@@STEP_LOG_LINE@.gitmodules@\tpath = b2@@@",
-      "@@@STEP_LOG_LINE@.gitmodules@\turl = sso://foo/b2@@@",
-      "@@@STEP_LOG_END@.gitmodules@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "a1"
-  },
-  {
-    "cmd": [
-      "git",
-      "ls-remote",
-      "--heads",
-      "https://foo.googlesource.com/a1",
-      "main"
-    ],
-    "name": "a1.git ls-remote",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_LOG_LINE@stdout@h3ll0\trefs/heads/main@@@",
-      "@@@STEP_LOG_END@stdout@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "submodule",
-      "update",
-      "--init",
-      "--jobs",
-      "4",
-      "[START_DIR]/checkout/a1"
-    ],
-    "cwd": "[START_DIR]/checkout",
-    "name": "a1.git submodule update",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "log",
-      "--max-count=1",
-      "--pretty=format:%H"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.get old revision",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@1111111111111111111111111111111111111111@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "fetch",
-      "origin",
-      "h3ll0"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.git fetch",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "checkout",
-      "FETCH_HEAD"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.git checkout",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "log",
-      "--max-count=1",
-      "--pretty=format:%H"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.get new revision",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@2222222222222222222222222222222222222222@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "a1.get roll direction",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@forward@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "merge-base",
-      "--is-ancestor",
-      "1111111111111111111111111111111111111111",
-      "2222222222222222222222222222222222222222"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.get roll direction.is forward",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "merge-base",
-      "--is-ancestor",
-      "2222222222222222222222222222222222222222",
-      "1111111111111111111111111111111111111111"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.get roll direction.is backward",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "a1.remote",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "remote"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.remote.name",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "remote",
-      "get-url",
-      "origin"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.remote.url",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "log",
-      "--pretty=format:%H\n%an\n%ae\n%B",
-      "-z",
-      "1111111111111111111111111111111111111111..2222222222222222222222222222222222222222"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "name": "a1.git log",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "a1.ensure infra/tools/luci/gerrit/${platform}",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "a1.ensure infra/tools/luci/gerrit/${platform}.get packages",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "vpython3",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "copy",
-      "RECIPE_MODULE[fuchsia::gerrit]/resources/cipd.ensure",
-      "/path/to/tmp/"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "infra_step": true,
-    "name": "a1.ensure infra/tools/luci/gerrit/${platform}.get packages.read ensure file",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@cipd.ensure@infra/tools/luci/gerrit/${platform} version:pinned-version@@@",
-      "@@@STEP_LOG_END@cipd.ensure@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "a1.ensure infra/tools/luci/gerrit/${platform}.install infra/tools/luci/gerrit",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "vpython3",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "ensure-directory",
-      "--mode",
-      "0o777",
-      "[START_DIR]/cipd_tool/infra/tools/luci/gerrit/0e548aa33f8113a45a5b3b62201e114e98e63d00f97296912380138f44597b07"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "infra_step": true,
-    "name": "a1.ensure infra/tools/luci/gerrit/${platform}.install infra/tools/luci/gerrit.ensure package directory",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@3@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "cipd",
-      "ensure",
-      "-root",
-      "[START_DIR]/cipd_tool/infra/tools/luci/gerrit/0e548aa33f8113a45a5b3b62201e114e98e63d00f97296912380138f44597b07",
-      "-ensure-file",
-      "infra/tools/luci/gerrit/${platform} version:pinned-version",
-      "-max-threads",
-      "0",
-      "-json-output",
-      "/path/to/tmp/json"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "infra_step": true,
-    "name": "a1.ensure infra/tools/luci/gerrit/${platform}.install infra/tools/luci/gerrit.ensure_installed",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@json.output@{@@@",
-      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
-      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
-      "@@@STEP_LOG_LINE@json.output@      {@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-version:pinned-v\",@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/luci/gerrit/resolved-platform\"@@@",
-      "@@@STEP_LOG_LINE@json.output@      }@@@",
-      "@@@STEP_LOG_LINE@json.output@    ]@@@",
-      "@@@STEP_LOG_LINE@json.output@  }@@@",
-      "@@@STEP_LOG_LINE@json.output@}@@@",
-      "@@@STEP_LOG_END@json.output@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "[START_DIR]/cipd_tool/infra/tools/luci/gerrit/0e548aa33f8113a45a5b3b62201e114e98e63d00f97296912380138f44597b07/gerrit",
-      "change-query",
-      "-host",
-      "https://pigweed-review.googlesource.com",
-      "-input",
-      "{\"params\": {\"q\": \"commit:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}}",
-      "-output",
-      "/path/to/tmp/json"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "infra_step": true,
-    "name": "a1.get change-id",
-    "timeout": 600,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_LOG_LINE@json.output@[@@@",
-      "@@@STEP_LOG_LINE@json.output@  {@@@",
-      "@@@STEP_LOG_LINE@json.output@    \"_number\": 12345@@@",
-      "@@@STEP_LOG_LINE@json.output@  }@@@",
-      "@@@STEP_LOG_LINE@json.output@]@@@",
-      "@@@STEP_LOG_END@json.output@@@",
-      "@@@STEP_LOG_LINE@json.input@{@@@",
-      "@@@STEP_LOG_LINE@json.input@  \"params\": {@@@",
-      "@@@STEP_LOG_LINE@json.input@    \"q\": \"commit:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"@@@",
-      "@@@STEP_LOG_LINE@json.input@  }@@@",
-      "@@@STEP_LOG_LINE@json.input@}@@@",
-      "@@@STEP_LOG_END@json.input@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "[START_DIR]/cipd_tool/infra/tools/luci/gerrit/0e548aa33f8113a45a5b3b62201e114e98e63d00f97296912380138f44597b07/gerrit",
-      "change-detail",
-      "-host",
-      "https://pigweed-review.googlesource.com",
-      "-input",
-      "{\"change_id\": \"12345\"}",
-      "-output",
-      "/path/to/tmp/json"
-    ],
-    "cwd": "[START_DIR]/checkout/a1",
-    "infra_step": true,
-    "name": "a1.get 12345",
-    "timeout": 600,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_LOG_LINE@json.output@{@@@",
-      "@@@STEP_LOG_LINE@json.output@  \"owner\": {@@@",
-      "@@@STEP_LOG_LINE@json.output@    \"email\": \"author@example.com\",@@@",
-      "@@@STEP_LOG_LINE@json.output@    \"name\": \"author\"@@@",
-      "@@@STEP_LOG_LINE@json.output@  },@@@",
-      "@@@STEP_LOG_LINE@json.output@  \"reviewers\": {@@@",
-      "@@@STEP_LOG_LINE@json.output@    \"REVIEWER\": [@@@",
-      "@@@STEP_LOG_LINE@json.output@      {@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"email\": \"reviewer@example.com\",@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"name\": \"reviewer\"@@@",
-      "@@@STEP_LOG_LINE@json.output@      },@@@",
-      "@@@STEP_LOG_LINE@json.output@      {@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"email\": \"nobody@google.com\",@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"name\": \"nobody\"@@@",
-      "@@@STEP_LOG_LINE@json.output@      },@@@",
-      "@@@STEP_LOG_LINE@json.output@      {@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"email\": \"robot@gserviceaccount.com\",@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"name\": \"robot\"@@@",
-      "@@@STEP_LOG_LINE@json.output@      }@@@",
-      "@@@STEP_LOG_LINE@json.output@    ]@@@",
-      "@@@STEP_LOG_LINE@json.output@  }@@@",
-      "@@@STEP_LOG_LINE@json.output@}@@@",
-      "@@@STEP_LOG_END@json.output@@@",
-      "@@@STEP_LOG_LINE@json.input@{@@@",
-      "@@@STEP_LOG_LINE@json.input@  \"change_id\": \"12345\"@@@",
-      "@@@STEP_LOG_LINE@json.input@}@@@",
-      "@@@STEP_LOG_END@json.input@@@",
-      "@@@STEP_LINK@gerrit link@https://pigweed-review.googlesource.com/q/12345@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "b2",
-    "~followup_annotations": [
-      "@@@STEP_SUMMARY_TEXT@no roll required@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "ls-remote",
-      "--heads",
-      "https://foo.googlesource.com/b2",
-      "main"
-    ],
-    "name": "b2.git ls-remote",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_LOG_LINE@stdout@h3ll0\trefs/heads/main@@@",
-      "@@@STEP_LOG_END@stdout@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "submodule",
-      "update",
-      "--init",
-      "--jobs",
-      "4",
-      "[START_DIR]/checkout/b2"
-    ],
-    "cwd": "[START_DIR]/checkout",
-    "name": "b2.git submodule update",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "log",
-      "--max-count=1",
-      "--pretty=format:%H"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "name": "b2.get old revision",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@1111111111111111111111111111111111111111@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "fetch",
-      "origin",
-      "h3ll0"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "name": "b2.git fetch",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "checkout",
-      "FETCH_HEAD"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "name": "b2.git checkout",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "log",
-      "--max-count=1",
-      "--pretty=format:%H"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "name": "b2.get new revision",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@2222222222222222222222222222222222222222@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "b2.get roll direction",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "merge-base",
-      "--is-ancestor",
-      "1111111111111111111111111111111111111111",
-      "2222222222222222222222222222222222222222"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "name": "b2.get roll direction.is forward",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "merge-base",
-      "--is-ancestor",
-      "2222222222222222222222222222222222222222",
-      "1111111111111111111111111111111111111111"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "name": "b2.get roll direction.is backward",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "b2.get roll direction.get roll direction",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@up-to-date@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "b2.cancelling roll",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@not updating from 1111111 to 2222222 because 1111111 is newer than 2222222@@@",
-      "@@@STEP_LINK@1111111111111111111111111111111111111111@https://foo.googlesource.com/b2/+/1111111111111111111111111111111111111111@@@",
-      "@@@STEP_LINK@2222222222222222222222222222222222222222@https://foo.googlesource.com/b2/+/2222222222222222222222222222222222222222@@@"
-    ]
-  },
-  {
-    "name": "$result"
-  }
-]
\ No newline at end of file
diff --git a/recipe_modules/submodule_roll/tests/full.expected/success.json b/recipe_modules/submodule_roll/tests/full.expected/success.json
index 74c224e..771ef81 100644
--- a/recipe_modules/submodule_roll/tests/full.expected/success.json
+++ b/recipe_modules/submodule_roll/tests/full.expected/success.json
@@ -371,263 +371,6 @@
     ]
   },
   {
-    "cmd": [],
-    "name": "b2"
-  },
-  {
-    "cmd": [
-      "git",
-      "ls-remote",
-      "--heads",
-      "https://foo.googlesource.com/b2",
-      "main"
-    ],
-    "name": "b2.git ls-remote",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_LOG_LINE@stdout@h3ll0\trefs/heads/main@@@",
-      "@@@STEP_LOG_END@stdout@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "submodule",
-      "update",
-      "--init",
-      "--jobs",
-      "4",
-      "[START_DIR]/checkout/b2"
-    ],
-    "cwd": "[START_DIR]/checkout",
-    "name": "b2.git submodule update",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "log",
-      "--max-count=1",
-      "--pretty=format:%H"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "name": "b2.get old revision",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@1111111111111111111111111111111111111111@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "fetch",
-      "origin",
-      "h3ll0"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "name": "b2.git fetch",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "checkout",
-      "FETCH_HEAD"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "name": "b2.git checkout",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "log",
-      "--max-count=1",
-      "--pretty=format:%H"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "name": "b2.get new revision",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@2222222222222222222222222222222222222222@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "b2.get roll direction",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@forward@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "merge-base",
-      "--is-ancestor",
-      "1111111111111111111111111111111111111111",
-      "2222222222222222222222222222222222222222"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "name": "b2.get roll direction.is forward",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "merge-base",
-      "--is-ancestor",
-      "2222222222222222222222222222222222222222",
-      "1111111111111111111111111111111111111111"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "name": "b2.get roll direction.is backward",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "b2.remote",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "remote"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "name": "b2.remote.name",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "remote",
-      "get-url",
-      "origin"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "name": "b2.remote.url",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "log",
-      "--pretty=format:%H\n%an\n%ae\n%B",
-      "-z",
-      "1111111111111111111111111111111111111111..2222222222222222222222222222222222222222"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "name": "b2.git log",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "[START_DIR]/cipd_tool/infra/tools/luci/gerrit/0e548aa33f8113a45a5b3b62201e114e98e63d00f97296912380138f44597b07/gerrit",
-      "change-query",
-      "-host",
-      "https://pigweed-review.googlesource.com",
-      "-input",
-      "{\"params\": {\"q\": \"commit:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}}",
-      "-output",
-      "/path/to/tmp/json"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "infra_step": true,
-    "name": "b2.get change-id",
-    "timeout": 600,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_LOG_LINE@json.output@[@@@",
-      "@@@STEP_LOG_LINE@json.output@  {@@@",
-      "@@@STEP_LOG_LINE@json.output@    \"_number\": 12345@@@",
-      "@@@STEP_LOG_LINE@json.output@  }@@@",
-      "@@@STEP_LOG_LINE@json.output@]@@@",
-      "@@@STEP_LOG_END@json.output@@@",
-      "@@@STEP_LOG_LINE@json.input@{@@@",
-      "@@@STEP_LOG_LINE@json.input@  \"params\": {@@@",
-      "@@@STEP_LOG_LINE@json.input@    \"q\": \"commit:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"@@@",
-      "@@@STEP_LOG_LINE@json.input@  }@@@",
-      "@@@STEP_LOG_LINE@json.input@}@@@",
-      "@@@STEP_LOG_END@json.input@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "[START_DIR]/cipd_tool/infra/tools/luci/gerrit/0e548aa33f8113a45a5b3b62201e114e98e63d00f97296912380138f44597b07/gerrit",
-      "change-detail",
-      "-host",
-      "https://pigweed-review.googlesource.com",
-      "-input",
-      "{\"change_id\": \"12345\"}",
-      "-output",
-      "/path/to/tmp/json"
-    ],
-    "cwd": "[START_DIR]/checkout/b2",
-    "infra_step": true,
-    "name": "b2.get 12345",
-    "timeout": 600,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_LOG_LINE@json.output@{@@@",
-      "@@@STEP_LOG_LINE@json.output@  \"owner\": {@@@",
-      "@@@STEP_LOG_LINE@json.output@    \"email\": \"author@example.com\",@@@",
-      "@@@STEP_LOG_LINE@json.output@    \"name\": \"author\"@@@",
-      "@@@STEP_LOG_LINE@json.output@  },@@@",
-      "@@@STEP_LOG_LINE@json.output@  \"reviewers\": {@@@",
-      "@@@STEP_LOG_LINE@json.output@    \"REVIEWER\": [@@@",
-      "@@@STEP_LOG_LINE@json.output@      {@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"email\": \"reviewer@example.com\",@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"name\": \"reviewer\"@@@",
-      "@@@STEP_LOG_LINE@json.output@      },@@@",
-      "@@@STEP_LOG_LINE@json.output@      {@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"email\": \"nobody@google.com\",@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"name\": \"nobody\"@@@",
-      "@@@STEP_LOG_LINE@json.output@      },@@@",
-      "@@@STEP_LOG_LINE@json.output@      {@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"email\": \"robot@gserviceaccount.com\",@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"name\": \"robot\"@@@",
-      "@@@STEP_LOG_LINE@json.output@      }@@@",
-      "@@@STEP_LOG_LINE@json.output@    ]@@@",
-      "@@@STEP_LOG_LINE@json.output@  }@@@",
-      "@@@STEP_LOG_LINE@json.output@}@@@",
-      "@@@STEP_LOG_END@json.output@@@",
-      "@@@STEP_LOG_LINE@json.input@{@@@",
-      "@@@STEP_LOG_LINE@json.input@  \"change_id\": \"12345\"@@@",
-      "@@@STEP_LOG_LINE@json.input@}@@@",
-      "@@@STEP_LOG_END@json.input@@@",
-      "@@@STEP_LINK@gerrit link@https://pigweed-review.googlesource.com/q/12345@@@"
-    ]
-  },
-  {
     "name": "$result"
   }
 ]
\ No newline at end of file
diff --git a/recipe_modules/submodule_roll/tests/full.proto b/recipe_modules/submodule_roll/tests/full.proto
index 2c4361a..44f0f53 100644
--- a/recipe_modules/submodule_roll/tests/full.proto
+++ b/recipe_modules/submodule_roll/tests/full.proto
@@ -22,5 +22,5 @@
 
 message InputProperties {
   // Submodules to update.
-  repeated recipe_modules.pigweed.submodule_roll.SubmoduleEntry submodules = 1;
+  recipe_modules.pigweed.submodule_roll.SubmoduleEntry submodule = 1;
 }
diff --git a/recipe_modules/submodule_roll/tests/full.py b/recipe_modules/submodule_roll/tests/full.py
index b51f940..95ee26b 100644
--- a/recipe_modules/submodule_roll/tests/full.py
+++ b/recipe_modules/submodule_roll/tests/full.py
@@ -33,7 +33,7 @@
 ):
     checkout = api.checkout.fake_context()
 
-    rolls = api.submodule_roll.update(checkout, props.submodules)
+    rolls = api.submodule_roll.update(checkout, props.submodule)
 
     if not rolls:
         with api.step.nest('nothing to roll, exiting'):
@@ -43,41 +43,30 @@
 def GenTests(api) -> Generator[recipe_test_api.TestData, None, None]:
     """Create tests."""
 
-    def properties(submodules, **kwargs):
+    def properties(*submodules, **kwargs):
         props = InputProperties(**kwargs)
-        props.submodules.extend(submodules)
+        assert len(submodules) == 1
+        props.submodule.CopyFrom(submodules[0])
         return api.properties(props)
 
     yield api.test(
         'success',
-        properties(api.submodule_roll.submodules('a1', 'b2')),
-        api.submodule_roll.commit_data('a1', prefix=''),
-        api.submodule_roll.commit_data('b2', prefix=''),
-        api.submodule_roll.gitmodules(a1='sso://foo/a1', b2='sso://foo/b2'),
-        api.roll_util.forward_roll('a1'),
-        api.roll_util.forward_roll('b2'),
-    )
-
-    yield api.test(
-        'partial_noop',
-        properties(api.submodule_roll.submodules('a1', 'b2')),
+        properties(*api.submodule_roll.submodules('a1')),
         api.submodule_roll.commit_data('a1', prefix=''),
         api.submodule_roll.gitmodules(a1='sso://foo/a1', b2='sso://foo/b2'),
         api.roll_util.forward_roll('a1'),
-        api.roll_util.noop_roll('b2'),
     )
 
     yield api.test(
         'noop',
-        properties(api.submodule_roll.submodules('a1', {'path': 'b2'})),
-        api.submodule_roll.gitmodules(a1='a1', b2='b2', b2_branch='branch'),
-        api.roll_util.noop_roll('a1'),
+        properties(*api.submodule_roll.submodules({'path': 'b2'})),
+        api.submodule_roll.gitmodules(b2='b2', b2_branch='branch'),
         api.roll_util.noop_roll('b2'),
     )
 
     yield api.test(
         'missing',
-        properties(api.submodule_roll.submodules('a1', 'b2')),
+        properties(*api.submodule_roll.submodules('b2')),
         api.submodule_roll.gitmodules(a1='sso://foo/a1'),
         status='FAILURE',
     )
diff --git a/recipes/submodule_roller.py b/recipes/submodule_roller.py
index df1e39a..1e75fc7 100644
--- a/recipes/submodule_roller.py
+++ b/recipes/submodule_roller.py
@@ -52,14 +52,16 @@
     props.checkout_options.use_trigger = False
     checkout = api.checkout(props.checkout_options)
 
-    rolls = api.submodule_roll.update(checkout, props.submodules)
+    rolls = []
+    for submodule in props.submodules:
+        rolls.extend(api.submodule_roll.update(checkout, submodule))
 
     if not rolls:
         with api.step.nest('nothing to roll, exiting'):
             return
 
-    authors = api.roll_util.authors(*rolls.values())
-    num_commits = sum(len(x.commits) for x in rolls.values())
+    authors = api.roll_util.authors(*rolls)
+    num_commits = sum(len(x.commits) for x in rolls)
 
     max_commits_for_ccing = props.max_commits_for_ccing or 10
     if num_commits <= max_commits_for_ccing:
@@ -67,7 +69,7 @@
         if cc_authors_on_rolls:
             cc.update(authors)
         if cc_reviewers_on_rolls:
-            cc.update(api.roll_util.reviewers(*rolls.values()))
+            cc.update(api.roll_util.reviewers(*rolls))
 
         def include_cc(email):
             return api.roll_util.include_cc(
@@ -99,7 +101,7 @@
     change = api.auto_roller.attempt_roll(
         complete_auto_roller_options,
         repo_dir=checkout.root,
-        commit_message=api.roll_util.message(*rolls.values()),
+        commit_message=api.roll_util.message(*rolls),
         author_override=author_override,
     )