checkout: Parse submodule state in resource script

Move parsing of submodule state (the .gitmodules file and the output of
'git submodule status --recursive') into a resource script that writes a
JSON file recipe code can parse. Update recipe code to use this script.

Also fix a small submodule_roller bug.

Change-Id: Ida75acf029a8124157996dfef933892d37f4a1b8
Reviewed-on: https://pigweed-review.googlesource.com/c/infra/recipes/+/92641
Commit-Queue: Rob Mohr <mohrr@google.com>
Reviewed-by: Oliver Newman <olivernewman@google.com>
diff --git a/recipe_modules/checkout/api.py b/recipe_modules/checkout/api.py
index 6b896f2..1839d93 100644
--- a/recipe_modules/checkout/api.py
+++ b/recipe_modules/checkout/api.py
@@ -134,10 +134,19 @@
     hash = attr.ib(type=str)
     relative_path = attr.ib(type=str)
     path = attr.ib(type=config_types.Path)
+    name = attr.ib(type=str)
     describe = attr.ib(type=str)
     remote = attr.ib(type=str)
     initialized = attr.ib(type=bool)
-    current = attr.ib(type=bool)
+    modified = attr.ib(type=bool)
+    conflict = attr.ib(type=bool)
+    branch = attr.ib(type=str)
+    url = attr.ib(type=str)
+    update = attr.ib(type=str)
+    ignore = attr.ib(type=str)
+    shallow = attr.ib(type=bool)
+    fetchRecurseSubmodules = attr.ib(type=bool)
+    describe = attr.ib(type=str)
 
 
 @attr.s
@@ -252,79 +261,34 @@
             'write git log', directory.join('git.log'), log,
         )
 
-    def _parse_submodule_status(self, line):
-        """Parse a `git submodule status` and get the remote URL."""
-        match = re.search(
-            r'^(?P<status>[+-]?)(?P<hash>[0-9a-fA-F]{40})\s+'
-            r'(?P<path>[^()]*?)\s*'
-            r'(?:\((?P<describe>[^()]*)\))?$',
-            line.strip(),
-        )
-        if not match:
-            raise self._api.step.InfraFailure(
-                'unrecognized submodule status line "{}"'.format(line)
-            )
-
-        with self._api.step.nest(match.group('path')) as pres:
-            pres.step_summary_text = 'hash={}\ndescribe={}'.format(
-                match.group('hash'), match.group('describe')
-            )
-        path = self.root.join(match.group('path'))
-        with self._api.context(cwd=path):
-            remote = self._api.git(
-                'git origin {}'.format(path),
-                'config',
-                '--get',
-                'remote.origin.url',
-                stdout=self._api.raw_io.output_text(),
-            ).stdout.strip()
-        remote_https = self._api.sso.sso_to_https(remote)
-        if remote_https.endswith('.git'):
-            remote_https = remote_https[0:-4]
-
-        if match.group('status') == '-':
-            initialized = current = False
-        elif match.group('status') == '+':
-            initialized = True
-            current = False
-        else:
-            initialized = current = True
-
-        return Submodule(
-            api=self._api,
-            hash=match.group('hash'),
-            relative_path=match.group('path'),
-            path=path,
-            describe=match.group('describe'),
-            remote=remote_https,
-            initialized=initialized,
-            current=current,
-        )
-
     def submodules(self, recursive=False):
         """Return data about all submodules."""
 
-        with self._api.context(cwd=self.root):
-            args = ['git submodule status', 'submodule', 'status']
-            if recursive:
-                args.append('--recursive')
-            kwargs = {
-                'stdout': self._api.raw_io.output_text(),
-                'step_test_data': lambda: self._api.raw_io.test_api.stream_output_text(
-                    ''
-                ),
-            }
-            submodule_status_lines = sorted(
-                self._api.git(*args, **kwargs).stdout.splitlines()
-            )
+        cmd = [
+            'python3',
+            self._api.checkout.resource('submodule_status.py'),
+            self.root,
+            self._api.json.output(),
+        ]
 
-            submodules = []
-            if submodule_status_lines:
-                with self._api.step.nest('parse submodules'):
-                    for line in submodule_status_lines:
-                        submodules.append(self._parse_submodule_status(line))
+        if recursive:
+            cmd.append('--recursive')
 
-            return submodules
+        submodules = []
+        submodule_status = self._api.step(
+            'submodule status',
+            cmd,
+            step_test_data=lambda: self._api.json.test_api.output({}),
+        ).json.output
+        for sub in submodule_status.values():
+            sub['remote'] = self._api.sso.sso_to_https(sub['remote'])
+            if sub['remote'].endswith('.git'):
+                sub['remote'] = sub['remote'][0:-4]
+            sub['relative_path'] = sub['path']
+            sub['path'] = self.root.join(sub['path'])
+            submodules.append(Submodule(self._api, **sub))
+
+        return submodules
 
     _REMOTE_REGEX = re.compile(r'^https://(?P<host>[^/]+)/(?P<project>.+)$')
 
diff --git a/recipe_modules/checkout/resources/submodule_status.py b/recipe_modules/checkout/resources/submodule_status.py
new file mode 100755
index 0000000..0ae5ac7
--- /dev/null
+++ b/recipe_modules/checkout/resources/submodule_status.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# TODO(mohrr) Write unit tests for this.
+
+import argparse
+import configparser
+import json
+import pathlib
+import re
+import subprocess
+import sys
+
+
+def debug(*args, **kwargs):
+    if True:
+        kwargs['file'] = sys.stderr
+        return print(*args, **kwargs)
+
+
+def trace(func):
+    def wrapped(*args, **kwargs):
+        try:
+            debug(f'== entering {func.__name__}')
+            res = func(*args, **kwargs)
+            debug(f'== exiting {func.__name__} with result {res}')
+            return res
+        except Exception as e:
+            debug(f'== exiting {func.__name__} with exception {e}')
+            raise
+
+    return wrapped
+
+
+@trace
+def _git(path, *cmd):
+    args = ['git'] + list(cmd)
+    debug('running', args)
+    proc = subprocess.run(args=args, cwd=path, capture_output=True)
+    debug('result:', proc.stdout)
+    return proc.stdout
+
+
+@trace
+def _parse_gitmodules_file(git_root, data, to_process, prefix=None):
+    debug('git_root', git_root)
+    debug('data.keys()', data.keys())
+    debug('len(to_process)', len(to_process))
+    debug('prefix', prefix)
+    gitmodules = git_root / '.gitmodules'
+    if not gitmodules.is_file():
+        return
+    parser = configparser.ConfigParser()
+    parser.read(gitmodules)
+    if prefix and prefix != '.':
+        prefix += '/'
+    else:
+        prefix = ''
+
+    for section in parser.sections():
+        match = re.search(r'^submodule "(.*)"$', section)
+        if not match:
+            raise ValueError(section)
+        name = match.group(1)
+        raw_path = parser[section]['path']
+        path = f'{prefix}{raw_path}'.replace('\\', '/')
+
+        data.setdefault(path, {})
+        data[path]['path'] = path
+        data[path]['name'] = f'{prefix}{name}'
+        data[path]['url'] = parser[section]['url']
+        data[path]['branch'] = parser[section].get('branch')
+        data[path]['update'] = parser[section].get('update')
+        data[path]['ignore'] = parser[section].get('ignore')
+        data[path]['shallow'] = parser[section].get('shallow')
+        data[path]['fetchRecurseSubmodules'] = parser[section].get(
+            'fetchRecurseSubmodules'
+        )
+
+        resolved_path = (git_root / raw_path).resolve()
+        debug('appending', resolved_path)
+        to_process.append(resolved_path)
+
+
+@trace
+def _parse_gitmodules(git_root, data, recursive):
+    seen_submodules = set()
+    to_process = [git_root]
+    while to_process:
+        curr = to_process.pop()
+        debug('processing', curr)
+        seen_submodules.add(curr)
+        _parse_gitmodules_file(
+            curr, data, to_process, prefix=str(curr.relative_to(git_root))
+        )
+        if not recursive:
+            to_process.clear()
+
+
+@trace
+def _add_remotes(git_root, data):
+    base = (
+        _git(git_root, 'config', '--get', 'remote.origin.url').decode().strip()
+    )
+
+    for submodule in data.values():
+        remote = submodule['url']
+        if remote.startswith('.'):
+            remote = '/'.join((base.rstrip('/'), remote.lstrip('/')))
+
+            changes = 1
+            while changes:
+                changes = 0
+
+                remote, n = re.subn(r'/\./', '/', remote)
+                changes += n
+
+                remote, n = re.subn(r'/[^/]+/\.\./', '/', remote)
+                changes += n
+
+        submodule['remote'] = remote
+
+
+@trace
+def _parse_submodule_status(line, data):
+    """Parse a `git submodule status` and get the remote URL."""
+    match = re.search(
+        r'^(?P<status>[+-U]?)(?P<hash>[0-9a-fA-F]{40})\s+'
+        r'(?P<path>[^()]*?)\s*'
+        r'(?:\((?P<describe>[^()]*)\))?$',
+        line.strip(),
+    )
+    if not match:
+        raise ValueError('unrecognized submodule status line "{}"'.format(line))
+
+    try:
+        submodule = data[match.group('path')]
+    except KeyError:
+        debug(data.keys())
+        raise
+    submodule['initialized'] = match.group('status') != '-'
+    submodule['modified'] = match.group('status') == '+'
+    submodule['conflict'] = match.group('status') == 'U'
+    submodule['hash'] = match.group('hash')
+    submodule['describe'] = match.group('describe')
+
+
+@trace
+def _add_status(git_root, data, recursive):
+    cmd = ['git', 'submodule', 'status']
+    if recursive:
+        cmd.append('--recursive')
+    debug('running', cmd)
+    proc = subprocess.run(cmd, cwd=git_root, capture_output=True)
+    debug('result:', proc.stdout.decode())
+    for line in proc.stdout.decode().splitlines():
+        _parse_submodule_status(line, data)
+
+
+@trace
+def main(git_root, output_file, recursive):
+    data = {}
+    _parse_gitmodules(git_root, data, recursive)
+    _add_remotes(git_root, data)
+    _add_status(git_root, data, recursive)
+
+    json.dump(data, output_file)
+
+
+@trace
+def parse(argv=None):
+    parser = argparse.ArgumentParser()
+    parser.add_argument('git_root', type=pathlib.Path)
+    parser.add_argument('output_file', type=argparse.FileType('w'))
+    parser.add_argument('--recursive', action='store_true')
+    return parser.parse_args(argv)
+
+
+if __name__ == '__main__':
+    main(**vars(parse()))
+    sys.exit(0)
diff --git a/recipe_modules/checkout/test_api.py b/recipe_modules/checkout/test_api.py
index 15e0480..4c19267 100644
--- a/recipe_modules/checkout/test_api.py
+++ b/recipe_modules/checkout/test_api.py
@@ -194,33 +194,56 @@
             ),
         )
 
-    def submodule(self, path, remote, status='', hash='a' * 40):
-        Submodule = collections.namedtuple(
-            'Submodule', 'path remote status hash'
+    def submodule(self, path, remote, status='', **kwargs):
+        result = {}
+        result['path'] = path
+        result['remote'] = remote
+        result['hash'] = kwargs.pop('hash', 'a' * 40)
+        result['initialized'] = kwargs.pop('initialized', True)
+        result['branch'] = kwargs.pop('branch', 'main')
+        result['name'] = kwargs.pop('name', path)
+        result['describe'] = kwargs.pop('describe', '')
+        result['modified'] = kwargs.pop('modified', False)
+        result['conflict'] = kwargs.pop('conflict', False)
+        result['url'] = kwargs.pop('url', remote)
+        result['update'] = kwargs.pop('update', None)
+        result['ignore'] = kwargs.pop('ignore', None)
+        result['shallow'] = kwargs.pop('shallow', False)
+        result['fetchRecurseSubmodules'] = kwargs.pop(
+            'fetchRecurseSubmodules', None
         )
-        return Submodule(path, remote, status, hash)
+        assert not kwargs
+
+        if status == '-':
+            result['initialized'] = True
+            result['modified'] = False
+            result['conflict'] = False
+        elif status == '+':
+            result['initialized'] = True
+            result['modified'] = True
+            result['conflict'] = False
+        elif status == 'U':  # pragma: no cover
+            # Including for completeness but this is not expected to be used.
+            result['initialized'] = True
+            result['modified'] = True
+            result['conflict'] = True
+        elif status == ' ':
+            result['initialized'] = False
+            result['modified'] = False
+            result['conflict'] = False
+
+        return result
 
     def submodules(self, *submodules, **kwargs):
         prefix = kwargs.pop('prefix', 'checkout pigweed.')
         checkout_root = kwargs.pop('checkout_root', '[START_DIR]/checkout')
-        raw_submodule_status = kwargs.pop('raw_submodule_status', None)
         assert not kwargs
 
-        output = raw_submodule_status or ''.join(
-            '{}{} {} ({})\n'.format(sub.status, sub.hash, sub.path, 'foo')
-            for sub in submodules
+        sub_data = {sub['path']: sub for sub in submodules}
+
+        res = self.step_data(
+            '{}submodule status'.format(prefix), self.m.json.output(sub_data),
         )
-        res = self.override_step_data(
-            '{}git submodule status'.format(prefix),
-            stdout=self.m.raw_io.output_text(output),
-        )
-        for sub in submodules:
-            res += self.step_data(
-                '{}parse submodules.git origin {}/{}'.format(
-                    prefix, checkout_root, sub.path
-                ),
-                stdout=self.m.raw_io.output_text(sub.remote),
-            )
 
         return res
 
diff --git a/recipe_modules/checkout/tests/git.expected/ci.json b/recipe_modules/checkout/tests/git.expected/ci.json
index c94f52f..d0c331d 100644
--- a/recipe_modules/checkout/tests/git.expected/ci.json
+++ b/recipe_modules/checkout/tests/git.expected/ci.json
@@ -981,9 +981,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -999,10 +1000,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
@@ -1256,11 +1258,11 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status"
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -1273,8 +1275,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
+    "name": "submodule status",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
   },
   {
     "cmd": [],
diff --git a/recipe_modules/checkout/tests/git.expected/not_in_gerrit.json b/recipe_modules/checkout/tests/git.expected/not_in_gerrit.json
index d9613ef..f25506e 100644
--- a/recipe_modules/checkout/tests/git.expected/not_in_gerrit.json
+++ b/recipe_modules/checkout/tests/git.expected/not_in_gerrit.json
@@ -613,16 +613,18 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
@@ -792,13 +794,16 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status"
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json"
     ],
-    "cwd": "[START_DIR]/checkout",
-    "name": "git submodule status",
-    "timeout": 600.0
+    "name": "submodule status",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
   },
   {
     "cmd": [],
diff --git a/recipe_modules/checkout/tests/git.expected/other.json b/recipe_modules/checkout/tests/git.expected/other.json
index 14ef1a6..3853e75 100644
--- a/recipe_modules/checkout/tests/git.expected/other.json
+++ b/recipe_modules/checkout/tests/git.expected/other.json
@@ -618,16 +618,18 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
@@ -797,13 +799,16 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status"
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json"
     ],
-    "cwd": "[START_DIR]/checkout",
-    "name": "git submodule status",
-    "timeout": 600.0
+    "name": "submodule status",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
   },
   {
     "cmd": [],
diff --git a/recipe_modules/checkout/tests/git.expected/try.json b/recipe_modules/checkout/tests/git.expected/try.json
index 3648932..4a351bf 100644
--- a/recipe_modules/checkout/tests/git.expected/try.json
+++ b/recipe_modules/checkout/tests/git.expected/try.json
@@ -1303,9 +1303,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -1321,10 +1322,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
@@ -1578,11 +1580,11 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status"
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout/foo",
+      "/path/to/tmp/json"
     ],
-    "cwd": "[START_DIR]/checkout/foo",
     "luci_context": {
       "realm": {
         "name": "project:try"
@@ -1595,8 +1597,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
+    "name": "submodule status",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
   },
   {
     "cmd": [],
diff --git a/recipe_modules/checkout/tests/submodule.expected/submodule-ci.json b/recipe_modules/checkout/tests/submodule.expected/submodule-ci.json
index f9acfc7..5e6a8f7 100644
--- a/recipe_modules/checkout/tests/submodule.expected/submodule-ci.json
+++ b/recipe_modules/checkout/tests/submodule.expected/submodule-ci.json
@@ -769,9 +769,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -787,153 +788,76 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.bar",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/bar",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/bar",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.foo",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/foo",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/foo",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.b/c/d",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/b/c/d",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/b/c/d",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.baz",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/baz",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/baz",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"b/c/d\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/b/c/d\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"bar\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/bar\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"baz\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"baz\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"baz\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/baz.git\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/baz.git\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"foo\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/foo\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipe_modules/checkout/tests/submodule.expected/submodule-try-equivalent.json b/recipe_modules/checkout/tests/submodule.expected/submodule-try-equivalent.json
index 963d035..1be455f 100644
--- a/recipe_modules/checkout/tests/submodule.expected/submodule-try-equivalent.json
+++ b/recipe_modules/checkout/tests/submodule.expected/submodule-try-equivalent.json
@@ -980,9 +980,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -998,153 +999,76 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.bar",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/bar",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/bar",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.foo",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/foo",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/foo",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.b/c/d",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/b/c/d",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/b/c/d",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.baz",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/baz",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/baz",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"b/c/d\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/b/c/d\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"bar\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/bar\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"baz\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"baz\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"baz\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/baz.git\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/baz.git\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"foo\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/foo\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipe_modules/checkout/tests/submodule.expected/submodule-try-gibberish.json b/recipe_modules/checkout/tests/submodule.expected/submodule-try-gibberish.json
deleted file mode 100644
index 8853ba2..0000000
--- a/recipe_modules/checkout/tests/submodule.expected/submodule-try-gibberish.json
+++ /dev/null
@@ -1,1021 +0,0 @@
-[
-  {
-    "cmd": [],
-    "name": "checkout pigweed",
-    "~followup_annotations": [
-      "@@@STEP_EXCEPTION@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.change data",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.change data.process gerrit changes",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.change data.process gerrit changes.0",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@3@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.change data.process gerrit changes.0.ensure gerrit",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@4@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "vpython",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "copy",
-      "RECIPE_MODULE[fuchsia::gerrit]/resources/tool_manifest.json",
-      "/path/to/tmp/json"
-    ],
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.change data.process gerrit changes.0.ensure gerrit.read manifest",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@5@@@",
-      "@@@STEP_LOG_LINE@tool_manifest.json@{@@@",
-      "@@@STEP_LOG_LINE@tool_manifest.json@  \"path\": \"path/to/gerrit\",@@@",
-      "@@@STEP_LOG_LINE@tool_manifest.json@  \"version\": \"version:pinned-version\"@@@",
-      "@@@STEP_LOG_LINE@tool_manifest.json@}@@@",
-      "@@@STEP_LOG_END@tool_manifest.json@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.change data.process gerrit changes.0.ensure gerrit.install path/to/gerrit",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@5@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "vpython",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "ensure-directory",
-      "--mode",
-      "0777",
-      "[START_DIR]/cipd_tool/path/to/gerrit/version%3Apinned-version"
-    ],
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.change data.process gerrit changes.0.ensure gerrit.install path/to/gerrit.ensure package directory",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@6@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "cipd",
-      "ensure",
-      "-root",
-      "[START_DIR]/cipd_tool/path/to/gerrit/version%3Apinned-version",
-      "-ensure-file",
-      "path/to/gerrit version:pinned-version",
-      "-max-threads",
-      "0",
-      "-json-output",
-      "/path/to/tmp/json"
-    ],
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.change data.process gerrit changes.0.ensure gerrit.install path/to/gerrit.ensure_installed",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@6@@@",
-      "@@@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\": \"path/to/gerrit\"@@@",
-      "@@@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/path/to/gerrit/version%3Apinned-version/gerrit",
-      "change-detail",
-      "-host",
-      "https://x-review.googlesource.com",
-      "-input",
-      "{\"change_id\": \"123456\", \"params\": {\"o\": [\"CURRENT_COMMIT\", \"CURRENT_REVISION\"]}}",
-      "-output",
-      "/path/to/tmp/json"
-    ],
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.change data.process gerrit changes.0.details",
-    "timeout": 30,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@4@@@",
-      "@@@STEP_LOG_LINE@json.output@{@@@",
-      "@@@STEP_LOG_LINE@json.output@  \"branch\": \"main\", @@@",
-      "@@@STEP_LOG_LINE@json.output@  \"current_revision\": \"ffffffffffffffffffffffffffffffffffffffff\", @@@",
-      "@@@STEP_LOG_LINE@json.output@  \"revisions\": {@@@",
-      "@@@STEP_LOG_LINE@json.output@    \"ffffffffffffffffffffffffffffffffffffffff\": {@@@",
-      "@@@STEP_LOG_LINE@json.output@      \"commit\": {@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"parents\": [@@@",
-      "@@@STEP_LOG_LINE@json.output@          {}@@@",
-      "@@@STEP_LOG_LINE@json.output@        ]@@@",
-      "@@@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\": \"123456\", @@@",
-      "@@@STEP_LOG_LINE@json.input@  \"params\": {@@@",
-      "@@@STEP_LOG_LINE@json.input@    \"o\": [@@@",
-      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_COMMIT\", @@@",
-      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_REVISION\"@@@",
-      "@@@STEP_LOG_LINE@json.input@    ]@@@",
-      "@@@STEP_LOG_LINE@json.input@  }@@@",
-      "@@@STEP_LOG_LINE@json.input@}@@@",
-      "@@@STEP_LOG_END@json.input@@@",
-      "@@@STEP_LINK@gerrit link@https://x-review.googlesource.com/q/123456@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@3@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "[START_DIR]/cipd_tool/path/to/gerrit/version%3Apinned-version/gerrit",
-      "change-detail",
-      "-host",
-      "https://x-review.googlesource.com",
-      "-input",
-      "{\"change_id\": \"123456\", \"params\": {\"o\": [\"CURRENT_COMMIT\", \"CURRENT_REVISION\"]}}",
-      "-output",
-      "/path/to/tmp/json"
-    ],
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps.details x:123456",
-    "timeout": 30,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@4@@@",
-      "@@@STEP_LOG_LINE@json.output@{@@@",
-      "@@@STEP_LOG_LINE@json.output@  \"current_revision\": \"HASH\", @@@",
-      "@@@STEP_LOG_LINE@json.output@  \"project\": \"project\", @@@",
-      "@@@STEP_LOG_LINE@json.output@  \"revisions\": {@@@",
-      "@@@STEP_LOG_LINE@json.output@    \"HASH\": {@@@",
-      "@@@STEP_LOG_LINE@json.output@      \"_number\": 1, @@@",
-      "@@@STEP_LOG_LINE@json.output@      \"commit\": {@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"message\": \"\", @@@",
-      "@@@STEP_LOG_LINE@json.output@        \"parents\": [@@@",
-      "@@@STEP_LOG_LINE@json.output@          {@@@",
-      "@@@STEP_LOG_LINE@json.output@            \"commit\": \"PARENT\"@@@",
-      "@@@STEP_LOG_LINE@json.output@          }@@@",
-      "@@@STEP_LOG_LINE@json.output@        ]@@@",
-      "@@@STEP_LOG_LINE@json.output@      }@@@",
-      "@@@STEP_LOG_LINE@json.output@    }@@@",
-      "@@@STEP_LOG_LINE@json.output@  }, @@@",
-      "@@@STEP_LOG_LINE@json.output@  \"status\": \"NEW\"@@@",
-      "@@@STEP_LOG_LINE@json.output@}@@@",
-      "@@@STEP_LOG_END@json.output@@@",
-      "@@@STEP_LOG_LINE@json.input@{@@@",
-      "@@@STEP_LOG_LINE@json.input@  \"change_id\": \"123456\", @@@",
-      "@@@STEP_LOG_LINE@json.input@  \"params\": {@@@",
-      "@@@STEP_LOG_LINE@json.input@    \"o\": [@@@",
-      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_COMMIT\", @@@",
-      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_REVISION\"@@@",
-      "@@@STEP_LOG_LINE@json.input@    ]@@@",
-      "@@@STEP_LOG_LINE@json.input@  }@@@",
-      "@@@STEP_LOG_LINE@json.input@}@@@",
-      "@@@STEP_LOG_END@json.input@@@",
-      "@@@STEP_LINK@gerrit link@https://x-review.googlesource.com/q/123456@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "[START_DIR]/cipd_tool/path/to/gerrit/version%3Apinned-version/gerrit",
-      "change-query",
-      "-host",
-      "https://x-review.googlesource.com",
-      "-input",
-      "{\"params\": {\"q\": \"commit:PARENT\"}}",
-      "-output",
-      "/path/to/tmp/json"
-    ],
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps.number PARENT",
-    "timeout": 30,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@4@@@",
-      "@@@STEP_LOG_LINE@json.output@[@@@",
-      "@@@STEP_LOG_LINE@json.output@  {@@@",
-      "@@@STEP_LOG_LINE@json.output@    \"_number\": 458@@@",
-      "@@@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:PARENT\"@@@",
-      "@@@STEP_LOG_LINE@json.input@  }@@@",
-      "@@@STEP_LOG_LINE@json.input@}@@@",
-      "@@@STEP_LOG_END@json.input@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "[START_DIR]/cipd_tool/path/to/gerrit/version%3Apinned-version/gerrit",
-      "change-detail",
-      "-host",
-      "https://x-review.googlesource.com",
-      "-input",
-      "{\"change_id\": \"458\", \"params\": {\"o\": [\"CURRENT_COMMIT\", \"CURRENT_REVISION\"]}}",
-      "-output",
-      "/path/to/tmp/json"
-    ],
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps.details x:458",
-    "timeout": 30,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@4@@@",
-      "@@@STEP_LOG_LINE@json.output@{@@@",
-      "@@@STEP_LOG_LINE@json.output@  \"current_revision\": \"HASH\", @@@",
-      "@@@STEP_LOG_LINE@json.output@  \"project\": \"project\", @@@",
-      "@@@STEP_LOG_LINE@json.output@  \"revisions\": {@@@",
-      "@@@STEP_LOG_LINE@json.output@    \"HASH\": {@@@",
-      "@@@STEP_LOG_LINE@json.output@      \"_number\": 1, @@@",
-      "@@@STEP_LOG_LINE@json.output@      \"commit\": {@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"message\": \"\", @@@",
-      "@@@STEP_LOG_LINE@json.output@        \"parents\": [@@@",
-      "@@@STEP_LOG_LINE@json.output@          {@@@",
-      "@@@STEP_LOG_LINE@json.output@            \"commit\": \"PARENT\"@@@",
-      "@@@STEP_LOG_LINE@json.output@          }@@@",
-      "@@@STEP_LOG_LINE@json.output@        ]@@@",
-      "@@@STEP_LOG_LINE@json.output@      }@@@",
-      "@@@STEP_LOG_LINE@json.output@    }@@@",
-      "@@@STEP_LOG_LINE@json.output@  }, @@@",
-      "@@@STEP_LOG_LINE@json.output@  \"status\": \"SUBMITTED\"@@@",
-      "@@@STEP_LOG_LINE@json.output@}@@@",
-      "@@@STEP_LOG_END@json.output@@@",
-      "@@@STEP_LOG_LINE@json.input@{@@@",
-      "@@@STEP_LOG_LINE@json.input@  \"change_id\": \"458\", @@@",
-      "@@@STEP_LOG_LINE@json.input@  \"params\": {@@@",
-      "@@@STEP_LOG_LINE@json.input@    \"o\": [@@@",
-      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_COMMIT\", @@@",
-      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_REVISION\"@@@",
-      "@@@STEP_LOG_LINE@json.input@    ]@@@",
-      "@@@STEP_LOG_LINE@json.input@  }@@@",
-      "@@@STEP_LOG_LINE@json.input@}@@@",
-      "@@@STEP_LOG_END@json.input@@@",
-      "@@@STEP_LINK@gerrit link@https://x-review.googlesource.com/q/458@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps.parents x:123456",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@4@@@",
-      "@@@STEP_SUMMARY_TEXT@all parents already submitted@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps.resolve deps for x:123456",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@4@@@",
-      "@@@STEP_SUMMARY_TEXT@no dependencies@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps.pass",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@4@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.change data.changes",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.change data.changes.x:123456",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_SUMMARY_TEXT@Change(number=123456, remote='https://x.googlesource.com/baz', ref='refs/changes/56/123456/7', rebase=True, branch='main', gerrit_name='x', submitted=False, base=None, base_type=None, is_merge=False)@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.no non-standard branch names",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.cache",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "vpython",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "ensure-directory",
-      "--mode",
-      "0777",
-      "[CACHE]/git"
-    ],
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.cache.ensure git cache dir",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "vpython",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "copy",
-      "",
-      "[CACHE]/git/.GUARD_FILE"
-    ],
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.cache.write git cache guard file",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "vpython",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "ensure-directory",
-      "--mode",
-      "0777",
-      "[CACHE]/git/pigweed.googlesource.com-pigweed-pigweed"
-    ],
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.cache.makedirs",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "init"
-    ],
-    "cwd": "[CACHE]/git/pigweed.googlesource.com-pigweed-pigweed",
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.cache.git init",
-    "timeout": 60.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "remote.origin.url",
-      "https://pigweed.googlesource.com/pigweed/pigweed"
-    ],
-    "cwd": "[CACHE]/git/pigweed.googlesource.com-pigweed-pigweed",
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.cache.remote set-url",
-    "timeout": 60.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "fetch.uriprotocols",
-      "https"
-    ],
-    "cwd": "[CACHE]/git/pigweed.googlesource.com-pigweed-pigweed",
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.cache.set fetch.uriprotocols",
-    "timeout": 60.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "fetch",
-      "--prune",
-      "--tags",
-      "origin",
-      "--no-recurse-submodules"
-    ],
-    "cwd": "[CACHE]/git/pigweed.googlesource.com-pigweed-pigweed",
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.cache.git fetch",
-    "timeout": 1200.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "checkout",
-      "-f",
-      "FETCH_HEAD"
-    ],
-    "cwd": "[CACHE]/git/pigweed.googlesource.com-pigweed-pigweed",
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.cache.git checkout",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "submodule",
-      "update",
-      "--recursive",
-      "--force",
-      "--jobs",
-      "4"
-    ],
-    "cwd": "[CACHE]/git/pigweed.googlesource.com-pigweed-pigweed",
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.cache.git submodule update",
-    "timeout": 600,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "vpython",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "remove",
-      "[CACHE]/git/.GUARD_FILE"
-    ],
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.cache.remove git cache guard file",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "vpython",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "copytree",
-      "--symlinks",
-      "[CACHE]/git/pigweed.googlesource.com-pigweed-pigweed",
-      "[START_DIR]/checkout"
-    ],
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.copy from cache",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "vpython",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "ensure-directory",
-      "--mode",
-      "0777",
-      "[START_DIR]/checkout"
-    ],
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.makedirs",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "init"
-    ],
-    "cwd": "[START_DIR]/checkout",
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.git init",
-    "timeout": 60.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "remote",
-      "add",
-      "origin",
-      "https://pigweed.googlesource.com/pigweed/pigweed"
-    ],
-    "cwd": "[START_DIR]/checkout",
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.git remote",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "fetch.uriprotocols",
-      "https"
-    ],
-    "cwd": "[START_DIR]/checkout",
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.set fetch.uriprotocols",
-    "timeout": 60.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "fetch",
-      "--tags",
-      "origin",
-      "main"
-    ],
-    "cwd": "[START_DIR]/checkout",
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.git fetch",
-    "timeout": 1200.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "checkout",
-      "-f",
-      "FETCH_HEAD"
-    ],
-    "cwd": "[START_DIR]/checkout",
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.git checkout",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "rev-parse",
-      "HEAD"
-    ],
-    "cwd": "[START_DIR]/checkout",
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.git rev-parse",
-    "timeout": 60.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "clean",
-      "-f",
-      "-d",
-      "-x"
-    ],
-    "cwd": "[START_DIR]/checkout",
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.git clean",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "rev-parse",
-      "HEAD"
-    ],
-    "cwd": "[START_DIR]/checkout",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.git rev-parse (2)",
-    "timeout": 60.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "submodule",
-      "status",
-      "--recursive"
-    ],
-    "cwd": "[START_DIR]/checkout",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_EXCEPTION@@@"
-    ]
-  },
-  {
-    "failure": {
-      "humanReason": "unrecognized submodule status line \"GIBBERISH!\""
-    },
-    "name": "$result"
-  }
-]
\ No newline at end of file
diff --git a/recipe_modules/checkout/tests/submodule.expected/submodule-try-multiple-cqdeps.json b/recipe_modules/checkout/tests/submodule.expected/submodule-try-multiple-cqdeps.json
index fbc7388..0b16f71 100644
--- a/recipe_modules/checkout/tests/submodule.expected/submodule-try-multiple-cqdeps.json
+++ b/recipe_modules/checkout/tests/submodule.expected/submodule-try-multiple-cqdeps.json
@@ -1720,9 +1720,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -1738,153 +1739,76 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.bar",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/bar",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/bar",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.foo",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/foo",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/foo",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.b/c/d",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/b/c/d",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/b/c/d",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.baz",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/baz",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/baz",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"b/c/d\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/b/c/d\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"bar\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/bar\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"baz\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"baz\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"baz\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/baz.git\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/baz.git\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"foo\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/foo\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipe_modules/checkout/tests/submodule.expected/submodule-try-multiple-one-missing-one-forbidden-cqdeps.json b/recipe_modules/checkout/tests/submodule.expected/submodule-try-multiple-one-missing-one-forbidden-cqdeps.json
index 3db772f..09801e4 100644
--- a/recipe_modules/checkout/tests/submodule.expected/submodule-try-multiple-one-missing-one-forbidden-cqdeps.json
+++ b/recipe_modules/checkout/tests/submodule.expected/submodule-try-multiple-one-missing-one-forbidden-cqdeps.json
@@ -1819,9 +1819,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -1837,153 +1838,76 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.bar",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/bar",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/bar",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.foo",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/foo",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/foo",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.b/c/d",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/b/c/d",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/b/c/d",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.baz",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/baz",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/baz",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"b/c/d\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/b/c/d\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"bar\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/bar\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"baz\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"baz\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"baz\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/baz.git\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/baz.git\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"foo\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/foo\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipe_modules/checkout/tests/submodule.expected/submodule-try-multiple-one-missing.json b/recipe_modules/checkout/tests/submodule.expected/submodule-try-multiple-one-missing.json
index 16120ba..725099f 100644
--- a/recipe_modules/checkout/tests/submodule.expected/submodule-try-multiple-one-missing.json
+++ b/recipe_modules/checkout/tests/submodule.expected/submodule-try-multiple-one-missing.json
@@ -1375,9 +1375,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -1393,153 +1394,76 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.bar",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/bar",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/bar",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.foo",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/foo",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/foo",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.b/c/d",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/b/c/d",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/b/c/d",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.baz",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/baz",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/baz",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"b/c/d\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/b/c/d\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"bar\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/bar\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"baz\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"baz\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"baz\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/baz.git\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/baz.git\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"foo\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/foo\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipe_modules/checkout/tests/submodule.expected/submodule-try-multiple.json b/recipe_modules/checkout/tests/submodule.expected/submodule-try-multiple.json
index 9bd6f80..3a92b30 100644
--- a/recipe_modules/checkout/tests/submodule.expected/submodule-try-multiple.json
+++ b/recipe_modules/checkout/tests/submodule.expected/submodule-try-multiple.json
@@ -1374,9 +1374,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -1392,153 +1393,76 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.bar",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/bar",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/bar",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.foo",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/foo",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/foo",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.b/c/d",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/b/c/d",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/b/c/d",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.baz",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/baz",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/baz",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"b/c/d\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/b/c/d\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"bar\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/bar\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"baz\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"baz\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"baz\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/baz.git\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/baz.git\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"foo\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/foo\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipe_modules/checkout/tests/submodule.expected/submodule-try-not-found.json b/recipe_modules/checkout/tests/submodule.expected/submodule-try-not-found.json
index 8fcf2fe..662d0b5 100644
--- a/recipe_modules/checkout/tests/submodule.expected/submodule-try-not-found.json
+++ b/recipe_modules/checkout/tests/submodule.expected/submodule-try-not-found.json
@@ -980,9 +980,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -998,153 +999,76 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.bar",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/bar",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/bar",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.foo",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/foo",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/foo",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.b/c/d",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/b/c/d",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/b/c/d",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.baz",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/baz",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/baz",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"b/c/d\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/b/c/d\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"bar\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/bar\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"baz\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"baz\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"baz\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/baz.git\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/baz.git\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"foo\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/foo\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipe_modules/checkout/tests/submodule.expected/submodule-try.json b/recipe_modules/checkout/tests/submodule.expected/submodule-try.json
index 480edbd..786e5e1 100644
--- a/recipe_modules/checkout/tests/submodule.expected/submodule-try.json
+++ b/recipe_modules/checkout/tests/submodule.expected/submodule-try.json
@@ -980,9 +980,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -998,153 +999,76 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.bar",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/bar",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/bar",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.foo",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/foo",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/foo",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.b/c/d",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/b/c/d",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/b/c/d",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "checkout pigweed.parse submodules.baz",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/baz",
-    "luci_context": {
-      "realm": {
-        "name": "project:try"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "checkout pigweed.parse submodules.git origin [START_DIR]/checkout/baz",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"b/c/d\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/b/c/d\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/b/c/d\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"bar\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/bar\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/bar\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"baz\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"baz\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"baz\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/baz.git\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/baz.git\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"foo\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://x.googlesource.com/foo\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://x.googlesource.com/foo\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipe_modules/checkout/tests/submodule.py b/recipe_modules/checkout/tests/submodule.py
index bd7d318..fcf63bf 100644
--- a/recipe_modules/checkout/tests/submodule.py
+++ b/recipe_modules/checkout/tests/submodule.py
@@ -52,7 +52,9 @@
         api.checkout.submodule('baz', 'https://x.googlesource.com/baz.git'),
         # Using 'b/c/d' instead of 'a/b/c' because an 'a/' prefix is treated
         # specially by Gerrit and the buildbucket module.
-        api.checkout.submodule('b/c/d', 'https://x.googlesource.com/b/c/d'),
+        api.checkout.submodule(
+            'b/c/d', 'https://x.googlesource.com/b/c/d', ' '
+        ),
     )
 
     yield (
@@ -71,14 +73,6 @@
     )
 
     yield (
-        api.status_check.test('submodule-try-gibberish', status='infra_failure')
-        + props()
-        + api.checkout.try_test_data(git_repo='https://x.googlesource.com/baz')
-        + api.checkout.submodules(raw_submodule_status='GIBBERISH!')
-        + api.checkout.all_changes_applied()
-    )
-
-    yield (
         api.status_check.test('submodule-try-not-found', status='infra_failure')
         + props()
         + api.checkout.try_test_data(git_repo='https://x.googlesource.com/xyz')
diff --git a/recipes/build.expected/basic.json b/recipes/build.expected/basic.json
index ee1086f..b486443 100644
--- a/recipes/build.expected/basic.json
+++ b/recipes/build.expected/basic.json
@@ -981,9 +981,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -999,10 +1000,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/docs_builder.expected/docs.json b/recipes/docs_builder.expected/docs.json
index c3a877c..4d9c724 100644
--- a/recipes/docs_builder.expected/docs.json
+++ b/recipes/docs_builder.expected/docs.json
@@ -618,16 +618,18 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/docs_builder.expected/docs_dry_run.json b/recipes/docs_builder.expected/docs_dry_run.json
index 2e364f8..aecc432 100644
--- a/recipes/docs_builder.expected/docs_dry_run.json
+++ b/recipes/docs_builder.expected/docs_dry_run.json
@@ -618,16 +618,18 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/envtest.expected/environment_variables.json b/recipes/envtest.expected/environment_variables.json
index 91394dd..ca403c1 100644
--- a/recipes/envtest.expected/environment_variables.json
+++ b/recipes/envtest.expected/environment_variables.json
@@ -618,16 +618,18 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/envtest.expected/fail.json b/recipes/envtest.expected/fail.json
index 569deb5..2ee0c32 100644
--- a/recipes/envtest.expected/fail.json
+++ b/recipes/envtest.expected/fail.json
@@ -981,9 +981,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -999,10 +1000,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/envtest.expected/pigweed.json b/recipes/envtest.expected/pigweed.json
index 67d0329..cb9e2b8 100644
--- a/recipes/envtest.expected/pigweed.json
+++ b/recipes/envtest.expected/pigweed.json
@@ -981,9 +981,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -999,10 +1000,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/envtest.expected/windows.json b/recipes/envtest.expected/windows.json
index 32e50a5..6a9b234 100644
--- a/recipes/envtest.expected/windows.json
+++ b/recipes/envtest.expected/windows.json
@@ -981,9 +981,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]\\resources\\submodule_status.py",
+      "[START_DIR]\\checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]\\checkout",
@@ -999,10 +1000,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/luci_config.expected/starlark.json b/recipes/luci_config.expected/starlark.json
index 40a0965..9c6d0b0 100644
--- a/recipes/luci_config.expected/starlark.json
+++ b/recipes/luci_config.expected/starlark.json
@@ -1326,9 +1326,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -1344,10 +1345,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/pw_presubmit.expected/sign.json b/recipes/pw_presubmit.expected/sign.json
index 472c160..2237440 100644
--- a/recipes/pw_presubmit.expected/sign.json
+++ b/recipes/pw_presubmit.expected/sign.json
@@ -981,9 +981,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -999,10 +1000,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/pw_presubmit.expected/step.json b/recipes/pw_presubmit.expected/step.json
index 23698f7..1bd3eb8 100644
--- a/recipes/pw_presubmit.expected/step.json
+++ b/recipes/pw_presubmit.expected/step.json
@@ -1326,9 +1326,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -1344,10 +1345,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/recipes.expected/cq_try.json b/recipes/recipes.expected/cq_try.json
index a02c189..7259e8b 100644
--- a/recipes/recipes.expected/cq_try.json
+++ b/recipes/recipes.expected/cq_try.json
@@ -1326,9 +1326,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -1344,10 +1345,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/run_script.expected/run_script.json b/recipes/run_script.expected/run_script.json
index 5677133..7b20eb3 100644
--- a/recipes/run_script.expected/run_script.json
+++ b/recipes/run_script.expected/run_script.json
@@ -618,16 +618,18 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/submodule_roller.expected/backwards.json b/recipes/submodule_roller.expected/backwards.json
index f51b449..1989c89 100644
--- a/recipes/submodule_roller.expected/backwards.json
+++ b/recipes/submodule_roller.expected/backwards.json
@@ -852,12 +852,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -870,8 +870,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
+    "name": "submodule status",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
   },
   {
     "cmd": [
diff --git a/recipes/submodule_roller.expected/failure-cc-authors.json b/recipes/submodule_roller.expected/failure-cc-authors.json
index e98db9f..e7b58f2 100644
--- a/recipes/submodule_roller.expected/failure-cc-authors.json
+++ b/recipes/submodule_roller.expected/failure-cc-authors.json
@@ -852,12 +852,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -870,8 +870,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
+    "name": "submodule status",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
   },
   {
     "cmd": [
diff --git a/recipes/submodule_roller.expected/no-revision.json b/recipes/submodule_roller.expected/no-revision.json
index d7b34f7..0b51c6b 100644
--- a/recipes/submodule_roller.expected/no-revision.json
+++ b/recipes/submodule_roller.expected/no-revision.json
@@ -505,14 +505,17 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
-    "name": "git submodule status",
-    "timeout": 600.0
+    "name": "submodule status",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
   },
   {
     "cmd": [
diff --git a/recipes/submodule_roller.expected/relative-dot.json b/recipes/submodule_roller.expected/relative-dot.json
index 32ca049..691acc5 100644
--- a/recipes/submodule_roller.expected/relative-dot.json
+++ b/recipes/submodule_roller.expected/relative-dot.json
@@ -852,12 +852,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -870,8 +870,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
+    "name": "submodule status",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
   },
   {
     "cmd": [
diff --git a/recipes/submodule_roller.expected/relative-dotdot-dotdot-always-cc-reviewers.json b/recipes/submodule_roller.expected/relative-dotdot-dotdot-always-cc-reviewers.json
index ca8ddf9..871232d 100644
--- a/recipes/submodule_roller.expected/relative-dotdot-dotdot-always-cc-reviewers.json
+++ b/recipes/submodule_roller.expected/relative-dotdot-dotdot-always-cc-reviewers.json
@@ -852,12 +852,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -870,8 +870,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
+    "name": "submodule status",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
   },
   {
     "cmd": [
diff --git a/recipes/submodule_roller.expected/relative-dotdot.json b/recipes/submodule_roller.expected/relative-dotdot.json
index 6a7cae0..c7d74b1 100644
--- a/recipes/submodule_roller.expected/relative-dotdot.json
+++ b/recipes/submodule_roller.expected/relative-dotdot.json
@@ -852,12 +852,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -870,8 +870,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
+    "name": "submodule status",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
   },
   {
     "cmd": [
diff --git a/recipes/submodule_roller.expected/success-sso-cc-authors.json b/recipes/submodule_roller.expected/success-sso-cc-authors.json
index f82924e..126a678 100644
--- a/recipes/submodule_roller.expected/success-sso-cc-authors.json
+++ b/recipes/submodule_roller.expected/success-sso-cc-authors.json
@@ -852,12 +852,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -870,8 +870,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
+    "name": "submodule status",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
   },
   {
     "cmd": [
diff --git a/recipes/submodule_roller.expected/too-many-skip-deps.json b/recipes/submodule_roller.expected/too-many-skip-deps.json
index 7fca1ca..1cf5668 100644
--- a/recipes/submodule_roller.expected/too-many-skip-deps.json
+++ b/recipes/submodule_roller.expected/too-many-skip-deps.json
@@ -852,12 +852,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -870,8 +870,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
+    "name": "submodule status",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
   },
   {
     "cmd": [
diff --git a/recipes/submodule_roller.expected/trigger-mismatch-equivalent.json b/recipes/submodule_roller.expected/trigger-mismatch-equivalent.json
index 4c1db9a..28a33e5 100644
--- a/recipes/submodule_roller.expected/trigger-mismatch-equivalent.json
+++ b/recipes/submodule_roller.expected/trigger-mismatch-equivalent.json
@@ -852,12 +852,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -870,8 +870,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
+    "name": "submodule status",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
   },
   {
     "cmd": [
diff --git a/recipes/submodule_roller.expected/trigger-mismatch.json b/recipes/submodule_roller.expected/trigger-mismatch.json
index 977e7da..1aa7ba9 100644
--- a/recipes/submodule_roller.expected/trigger-mismatch.json
+++ b/recipes/submodule_roller.expected/trigger-mismatch.json
@@ -852,12 +852,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -870,8 +870,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
+    "name": "submodule status",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
   },
   {
     "failure": {
diff --git a/recipes/submodule_roller.expected/with-branch-prop-filter-emails.json b/recipes/submodule_roller.expected/with-branch-prop-filter-emails.json
index c7ca7a6..f15e86b 100644
--- a/recipes/submodule_roller.expected/with-branch-prop-filter-emails.json
+++ b/recipes/submodule_roller.expected/with-branch-prop-filter-emails.json
@@ -505,14 +505,17 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
-    "name": "git submodule status",
-    "timeout": 600.0
+    "name": "submodule status",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
   },
   {
     "cmd": [
diff --git a/recipes/submodule_roller.expected/with-requires-already-applied.json b/recipes/submodule_roller.expected/with-requires-already-applied.json
index d1aa2d3..02c60d9 100644
--- a/recipes/submodule_roller.expected/with-requires-already-applied.json
+++ b/recipes/submodule_roller.expected/with-requires-already-applied.json
@@ -855,12 +855,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -873,113 +873,59 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules"
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.eggs",
+    "name": "submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/eggs",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/eggs",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.ham",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/ham",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/ham",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.spam",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/spam",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/spam",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"eggs\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/eggs\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"ham\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/ham\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"spam\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/spam\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
@@ -1841,7 +1787,7 @@
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
       "HASH"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/ham",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -1868,7 +1814,7 @@
       "HASH",
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/ham",
     "luci_context": {
       "realm": {
         "name": "project:ci"
diff --git a/recipes/submodule_roller.expected/with-requires-child.json b/recipes/submodule_roller.expected/with-requires-child.json
index 19903cc..7c5bf97 100644
--- a/recipes/submodule_roller.expected/with-requires-child.json
+++ b/recipes/submodule_roller.expected/with-requires-child.json
@@ -855,12 +855,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -873,113 +873,59 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules"
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.eggs",
+    "name": "submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/eggs",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/eggs",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.ham",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/ham",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/ham",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.spam",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/spam",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/spam",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"eggs\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/eggs\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"ham\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/ham\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"spam\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/spam\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
@@ -1920,7 +1866,7 @@
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
       "HASH"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/ham",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -1947,7 +1893,7 @@
       "HASH",
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/ham",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -2369,7 +2315,7 @@
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
       "HASH"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/ham",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -2396,7 +2342,7 @@
       "HASH",
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/ham",
     "luci_context": {
       "realm": {
         "name": "project:ci"
diff --git a/recipes/submodule_roller.expected/with-requires-forbidden.json b/recipes/submodule_roller.expected/with-requires-forbidden.json
index b5a4356..598d492 100644
--- a/recipes/submodule_roller.expected/with-requires-forbidden.json
+++ b/recipes/submodule_roller.expected/with-requires-forbidden.json
@@ -855,12 +855,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -873,113 +873,59 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules"
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.eggs",
+    "name": "submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/eggs",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/eggs",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.ham",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/ham",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/ham",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.spam",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/spam",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/spam",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"eggs\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/eggs\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"ham\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/ham\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"spam\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/spam\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/submodule_roller.expected/with-requires-loop.json b/recipes/submodule_roller.expected/with-requires-loop.json
index f63207b..d39376c 100644
--- a/recipes/submodule_roller.expected/with-requires-loop.json
+++ b/recipes/submodule_roller.expected/with-requires-loop.json
@@ -855,12 +855,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -873,113 +873,59 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules"
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.eggs",
+    "name": "submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/eggs",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/eggs",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.ham",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/ham",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/ham",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.spam",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/spam",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/spam",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"eggs\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/eggs\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"ham\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/ham\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"spam\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/spam\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
@@ -1842,7 +1788,7 @@
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
       "HASH"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/ham",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -1869,7 +1815,7 @@
       "HASH",
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/ham",
     "luci_context": {
       "realm": {
         "name": "project:ci"
diff --git a/recipes/submodule_roller.expected/with-requires-not-in-checkout.json b/recipes/submodule_roller.expected/with-requires-not-in-checkout.json
index e4f0e18..3986380 100644
--- a/recipes/submodule_roller.expected/with-requires-not-in-checkout.json
+++ b/recipes/submodule_roller.expected/with-requires-not-in-checkout.json
@@ -855,12 +855,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -873,113 +873,59 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules"
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.eggs",
+    "name": "submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/eggs",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/eggs",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.ham",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/ham",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/ham",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.spam",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/spam",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/spam",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"eggs\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/eggs\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"ham\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/ham\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"spam\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/spam\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/submodule_roller.expected/with-requires-parent.json b/recipes/submodule_roller.expected/with-requires-parent.json
index 3d90e9e..d6dc35f 100644
--- a/recipes/submodule_roller.expected/with-requires-parent.json
+++ b/recipes/submodule_roller.expected/with-requires-parent.json
@@ -855,12 +855,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -873,113 +873,59 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules"
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.eggs",
+    "name": "submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/eggs",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/eggs",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.ham",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/ham",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/ham",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.spam",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/spam",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/spam",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"eggs\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/eggs\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"ham\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/ham\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"spam\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/spam\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
@@ -1920,7 +1866,7 @@
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
       "HASH"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/ham",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -1947,7 +1893,7 @@
       "HASH",
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/ham",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -2369,7 +2315,7 @@
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
       "HASH"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/ham",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -2396,7 +2342,7 @@
       "HASH",
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/ham",
     "luci_context": {
       "realm": {
         "name": "project:ci"
diff --git a/recipes/submodule_roller.expected/with-requires-transitive.json b/recipes/submodule_roller.expected/with-requires-transitive.json
index b4cb496..c3338f4 100644
--- a/recipes/submodule_roller.expected/with-requires-transitive.json
+++ b/recipes/submodule_roller.expected/with-requires-transitive.json
@@ -855,12 +855,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -873,113 +873,59 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules"
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.eggs",
+    "name": "submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/eggs",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/eggs",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.ham",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/ham",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/ham",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.spam",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/spam",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/spam",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"eggs\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/eggs\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"ham\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/ham\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"spam\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/spam\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
@@ -1919,7 +1865,7 @@
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
       "HASH"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/ham",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -1946,7 +1892,7 @@
       "HASH",
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/ham",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -2368,7 +2314,7 @@
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
       "HASH"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/eggs",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -2395,7 +2341,7 @@
       "HASH",
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/eggs",
     "luci_context": {
       "realm": {
         "name": "project:ci"
diff --git a/recipes/submodule_roller.expected/with-requires.json b/recipes/submodule_roller.expected/with-requires.json
index ab46510..d91f74a 100644
--- a/recipes/submodule_roller.expected/with-requires.json
+++ b/recipes/submodule_roller.expected/with-requires.json
@@ -855,12 +855,12 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
-    "cwd": "[START_DIR]/checkout",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -873,113 +873,59 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "git submodule status",
-    "timeout": 600.0
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules"
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.eggs",
+    "name": "submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/eggs",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/eggs",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.ham",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/ham",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/ham",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "parse submodules.spam",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_SUMMARY_TEXT@hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ndescribe=foo@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "git",
-      "config",
-      "--get",
-      "remote.origin.url"
-    ],
-    "cwd": "[START_DIR]/checkout/spam",
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
-    "name": "parse submodules.git origin [START_DIR]/checkout/spam",
-    "timeout": 600.0,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"eggs\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/eggs\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/eggs\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"ham\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/ham\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/ham\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"spam\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"branch\": \"main\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"conflict\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"describe\": \"\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"fetchRecurseSubmodules\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"hash\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ignore\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"initialized\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"modified\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"name\": \"spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"path\": \"spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"remote\": \"https://foo.googlesource.com/spam\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"shallow\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"update\": null, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"url\": \"https://foo.googlesource.com/spam\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
@@ -1842,7 +1788,7 @@
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
       "HASH"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/ham",
     "luci_context": {
       "realm": {
         "name": "project:ci"
@@ -1869,7 +1815,7 @@
       "HASH",
       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
     ],
-    "cwd": "[START_DIR]/checkout/spam",
+    "cwd": "[START_DIR]/checkout/ham",
     "luci_context": {
       "realm": {
         "name": "project:ci"
diff --git a/recipes/submodule_roller.py b/recipes/submodule_roller.py
index 66c845f..c19a095 100644
--- a/recipes/submodule_roller.py
+++ b/recipes/submodule_roller.py
@@ -158,8 +158,9 @@
                         dep.remote,
                         dep.commit,
                     )
+
                 direction = api.roll_util.get_roll_direction(
-                    submodule_dir, old_revision, dep.commit
+                    sub.path, old_revision, dep.commit
                 )
 
                 if api.roll_util.can_roll(direction):
@@ -546,8 +547,8 @@
             + gitmodules(spam='spam', ham='ham')
             + api.checkout.submodules(
                 sub('spam', 'https://foo.googlesource.com/spam', '-'),
-                sub('ham', 'https://foo.googlesource.com/ham', '-'),
-                sub('eggs', 'https://foo.googlesource.com/eggs', '-'),
+                sub('ham', 'https://foo.googlesource.com/ham', ' '),
+                sub('eggs', 'https://foo.googlesource.com/eggs', ' '),
                 prefix='',
             )
             + api.cq_deps.details(
diff --git a/recipes/target_to_cipd.expected/pw-presubmit.json b/recipes/target_to_cipd.expected/pw-presubmit.json
index e13507b..2f69134 100644
--- a/recipes/target_to_cipd.expected/pw-presubmit.json
+++ b/recipes/target_to_cipd.expected/pw-presubmit.json
@@ -618,16 +618,18 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/target_to_git.expected/success.json b/recipes/target_to_git.expected/success.json
index 2586f87..3de9dc7 100644
--- a/recipes/target_to_git.expected/success.json
+++ b/recipes/target_to_git.expected/success.json
@@ -981,9 +981,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -999,10 +1000,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/tokendb_check.expected/addition.json b/recipes/tokendb_check.expected/addition.json
index e9e5e00..eb39943 100644
--- a/recipes/tokendb_check.expected/addition.json
+++ b/recipes/tokendb_check.expected/addition.json
@@ -1449,9 +1449,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -1467,10 +1468,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/tokendb_check.expected/no-change.json b/recipes/tokendb_check.expected/no-change.json
index e9e5e00..eb39943 100644
--- a/recipes/tokendb_check.expected/no-change.json
+++ b/recipes/tokendb_check.expected/no-change.json
@@ -1449,9 +1449,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -1467,10 +1468,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/tokendb_check.expected/removal.json b/recipes/tokendb_check.expected/removal.json
index 781a4a6..f1e0875 100644
--- a/recipes/tokendb_check.expected/removal.json
+++ b/recipes/tokendb_check.expected/removal.json
@@ -1449,9 +1449,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -1467,10 +1468,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/tokendb_updater.expected/dry-run.json b/recipes/tokendb_updater.expected/dry-run.json
index 4c7a95b..13ad0ec 100644
--- a/recipes/tokendb_updater.expected/dry-run.json
+++ b/recipes/tokendb_updater.expected/dry-run.json
@@ -618,16 +618,18 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/tokendb_updater.expected/separate-repo.json b/recipes/tokendb_updater.expected/separate-repo.json
index 45b0f71..ea6d6de 100644
--- a/recipes/tokendb_updater.expected/separate-repo.json
+++ b/recipes/tokendb_updater.expected/separate-repo.json
@@ -618,16 +618,18 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/tokendb_updater.expected/simple.json b/recipes/tokendb_updater.expected/simple.json
index f334967..c63ea06 100644
--- a/recipes/tokendb_updater.expected/simple.json
+++ b/recipes/tokendb_updater.expected/simple.json
@@ -618,16 +618,18 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/txt_roller.expected/backwards.json b/recipes/txt_roller.expected/backwards.json
index 4a40431..0cd3f26 100644
--- a/recipes/txt_roller.expected/backwards.json
+++ b/recipes/txt_roller.expected/backwards.json
@@ -1027,16 +1027,18 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/project",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/project",
-    "name": "checkout foo.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout foo.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/txt_roller.expected/no-trigger.json b/recipes/txt_roller.expected/no-trigger.json
index 800c7ab..0b76612 100644
--- a/recipes/txt_roller.expected/no-trigger.json
+++ b/recipes/txt_roller.expected/no-trigger.json
@@ -1027,16 +1027,18 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/project",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/project",
-    "name": "checkout foo.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout foo.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/txt_roller.expected/success.json b/recipes/txt_roller.expected/success.json
index a47069f..238b9ef 100644
--- a/recipes/txt_roller.expected/success.json
+++ b/recipes/txt_roller.expected/success.json
@@ -1678,9 +1678,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/project",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/project",
@@ -1696,10 +1697,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout foo.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout foo.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/update_python_versions.expected/simple.json b/recipes/update_python_versions.expected/simple.json
index dd53297..329ab9a 100644
--- a/recipes/update_python_versions.expected/simple.json
+++ b/recipes/update_python_versions.expected/simple.json
@@ -618,16 +618,18 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/xrefs.expected/dry_run.json b/recipes/xrefs.expected/dry_run.json
index 84fe360..ee4a860 100644
--- a/recipes/xrefs.expected/dry_run.json
+++ b/recipes/xrefs.expected/dry_run.json
@@ -618,16 +618,18 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/xrefs.expected/kythe.json b/recipes/xrefs.expected/kythe.json
index 2a63407..72ea7f1 100644
--- a/recipes/xrefs.expected/kythe.json
+++ b/recipes/xrefs.expected/kythe.json
@@ -618,16 +618,18 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {
diff --git a/recipes/xrefs.expected/tryjob.json b/recipes/xrefs.expected/tryjob.json
index f01f59f..e91ca4e 100644
--- a/recipes/xrefs.expected/tryjob.json
+++ b/recipes/xrefs.expected/tryjob.json
@@ -1326,9 +1326,10 @@
   },
   {
     "cmd": [
-      "git",
-      "submodule",
-      "status",
+      "python3",
+      "RECIPE_MODULE[pigweed::checkout]/resources/submodule_status.py",
+      "[START_DIR]/checkout",
+      "/path/to/tmp/json",
       "--recursive"
     ],
     "cwd": "[START_DIR]/checkout",
@@ -1344,10 +1345,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "checkout pigweed.git submodule status",
-    "timeout": 600.0,
+    "name": "checkout pigweed.submodule status",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@json.output@{}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
     ]
   },
   {