afdo: Provide build endpoint for auto-committing builder changes.

Implement build API for go/cros-recipe-builder-commits, includes
providing a new endpoint. This API only supports updating kernel
afdo metadata for now but can be extended to support more in the
future.

Leave commit footers empty for now.

BUG=chromium:1081332
TEST=build_api test passed locally; unittests passed

Change-Id: I69509482b2e0875987714c9b3a3ea194a9aeea03
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/2628347
Reviewed-by: Alex Klein <saklein@chromium.org>
Reviewed-by: LaMont Jones <lamontjones@chromium.org>
Tested-by: Tiancong Wang <tcwang@google.com>
Commit-Queue: Tiancong Wang <tcwang@google.com>
diff --git a/api/controller/toolchain.py b/api/controller/toolchain.py
index 77dccf7..a889b3d 100644
--- a/api/controller/toolchain.py
+++ b/api/controller/toolchain.py
@@ -79,6 +79,11 @@
                   toolchain_util.BundleArtifacts),
 }
 
+_TOOLCHAIN_COMMIT_HANDLERS = {
+    BuilderConfig.Artifacts.VERIFIED_KERNEL_CWP_AFDO_FILE:
+        'VerifiedKernelCwpAfdoFile'
+}
+
 
 def _GetProfileInfoDict(profile_info):
   """Convert profile_info to a dict.
@@ -227,6 +232,54 @@
           art_info.artifacts.add().path = artifact
 
 
+def _GetUpdatedFilesResponse(_input_proto, output_proto, _config):
+  """Add successful status to the faux response."""
+  file_info = output_proto.updated_files.add()
+  file_info.path = '/any/modified/file'
+  output_proto.commit_message = 'Commit message'
+
+
+@faux.empty_error
+@faux.success(_GetUpdatedFilesResponse)
+@validate.require('uploaded_artifacts')
+@validate.validation_complete
+def GetUpdatedFiles(input_proto, output_proto, _config):
+  """Use uploaded artifacts to update some updates in a chromeos checkout.
+
+  The function will call toolchain_util.GetUpdatedFiles using the type of
+  uploaded artifacts to make some changes in a checkout, and return the list
+  of change files together with commit message.
+     updated_artifacts: A list of UpdatedArtifacts type which contains a tuple
+        of artifact info and profile info.
+  Note: the actual creation of the commit is done by CI, not here.
+
+  Args:
+    input_proto (GetUpdatedFilesRequest): The input proto
+    output_proto (GetUpdatedFilesResponse): The output proto
+    _config (api_config.ApiConfig): The API call config.
+  """
+  commit_message = ''
+  for artifact in input_proto.uploaded_artifacts:
+    artifact_type = artifact.artifact_info.artifact_type
+    if artifact_type not in _TOOLCHAIN_COMMIT_HANDLERS:
+      logging.error('%s not understood', artifact_type)
+      return controller.RETURN_CODE_UNRECOVERABLE
+    artifact_name = _TOOLCHAIN_COMMIT_HANDLERS[artifact_type]
+    if artifact_name:
+      assert len(artifact.artifact_info.artifacts) == 1, (
+          'Only one file to update per each artifact')
+      updated_files, message = toolchain_util.GetUpdatedFiles(
+          artifact_name, artifact.artifact_info.artifacts[0].path,
+          _GetProfileInfoDict(artifact.profile_info))
+      for f in updated_files:
+        file_info = output_proto.updated_files.add()
+        file_info.path = f
+
+      commit_message += message + '\n'
+    output_proto.commit_message = commit_message
+    # No commit footer is added for now. Can add more here if needed
+
+
 # TODO(crbug/1019868): Remove legacy code when cbuildbot builders are gone.
 _NAMES_FOR_AFDO_ARTIFACTS = {
     toolchain_pb2.ORDERFILE: 'orderfile',
diff --git a/api/controller/toolchain_unittest.py b/api/controller/toolchain_unittest.py
index 6f63a1b..b8d61ef 100644
--- a/api/controller/toolchain_unittest.py
+++ b/api/controller/toolchain_unittest.py
@@ -372,3 +372,60 @@
             artifacts=[
                 artifacts_pb2.Artifact(path=self.bundle.return_value[0])
             ]))
+
+
+class GetUpdatedFilesTest(cros_test_lib.MockTempDirTestCase,
+                          api_config.ApiConfigMixin):
+  """Unittests for GetUpdatedFiles."""
+
+  def setUp(self):
+    self.response = toolchain_pb2.GetUpdatedFilesResponse()
+    self.artifact_path = '/any/path/to/metadata'
+    self.profile_info = common_pb2.ArtifactProfileInfo(kernel_version='4.4')
+    self.update = self.PatchObject(
+        toolchain_util, 'GetUpdatedFiles', return_value=([], ''))
+
+  def _GetRequest(self, uploaded_artifacts):
+    uploaded = []
+    for artifact_type, artifact_path, profile_info in uploaded_artifacts:
+      uploaded.append(
+          toolchain_pb2.GetUpdatedFilesRequest.UploadedArtifacts(
+              artifact_info=toolchain_pb2.ArtifactInfo(
+                  artifact_type=artifact_type,
+                  artifacts=[artifacts_pb2.Artifact(path=artifact_path)]),
+              profile_info=profile_info))
+    return toolchain_pb2.GetUpdatedFilesRequest(uploaded_artifacts=uploaded)
+
+  def testRaisesForUnknown(self):
+    request = self._GetRequest([
+        (BuilderConfig.Artifacts.UNVERIFIED_KERNEL_CWP_AFDO_FILE,
+         self.artifact_path, self.profile_info)
+    ])
+    self.assertEqual(
+        controller.RETURN_CODE_UNRECOVERABLE,
+        toolchain.GetUpdatedFiles(request, self.response, self.api_config))
+
+  def testValidateOnly(self):
+    """Sanity check that a validate only call does not execute any logic."""
+    request = self._GetRequest([
+        (BuilderConfig.Artifacts.VERIFIED_KERNEL_CWP_AFDO_FILE,
+         self.artifact_path, self.profile_info)
+    ])
+    toolchain.GetUpdatedFiles(request, self.response, self.validate_only_config)
+
+  def testUpdateSuccess(self):
+    updated_file = '/path/to/updated_file'
+    self.update.return_value = ([updated_file], 'Commit Message')
+    request = self._GetRequest([
+        (BuilderConfig.Artifacts.VERIFIED_KERNEL_CWP_AFDO_FILE,
+         self.artifact_path, self.profile_info)
+    ])
+    toolchain.GetUpdatedFiles(request, self.response, self.api_config)
+    print(self.response.updated_files)
+    self.assertEqual(len(self.response.updated_files), 1)
+    self.assertEqual(
+        self.response.updated_files[0],
+        toolchain_pb2.GetUpdatedFilesResponse.UpdatedFile(path=updated_file),
+    )
+    self.assertIn('Commit Message', self.response.commit_message)
+    self.assertEqual(len(self.response.commit_footer), 0)
diff --git a/api/gen/chromite/api/toolchain_pb2.py b/api/gen/chromite/api/toolchain_pb2.py
index 75222a5..9fd86d7 100644
--- a/api/gen/chromite/api/toolchain_pb2.py
+++ b/api/gen/chromite/api/toolchain_pb2.py
@@ -25,7 +25,7 @@
   package='chromite.api',
   syntax='proto3',
   serialized_options=_b('Z6go.chromium.org/chromiumos/infra/proto/go/chromite/api'),
-  serialized_pb=_b('\n\x1c\x63hromite/api/toolchain.proto\x12\x0c\x63hromite.api\x1a\x1c\x63hromite/api/artifacts.proto\x1a\x1c\x63hromite/api/build_api.proto\x1a\x1a\x63hromite/api/sysroot.proto\x1a\x1f\x63hromiumos/builder_config.proto\x1a\x17\x63hromiumos/common.proto\"\x83\x01\n\x0c\x41rtifactInfo\x12H\n\rartifact_type\x18\x01 \x01(\x0e\x32\x31.chromiumos.BuilderConfig.Artifacts.ArtifactTypes\x12)\n\tartifacts\x18\x02 \x03(\x0b\x32\x16.chromite.api.Artifact\"\x83\x03\n\x1fPrepareForToolchainBuildRequest\x12I\n\x0e\x61rtifact_types\x18\x01 \x03(\x0e\x32\x31.chromiumos.BuilderConfig.Artifacts.ArtifactTypes\x12\"\n\x06\x63hroot\x18\x02 \x01(\x0b\x32\x12.chromiumos.Chroot\x12&\n\x07sysroot\x18\x03 \x01(\x0b\x32\x15.chromite.api.Sysroot\x12N\n\x0finput_artifacts\x18\x04 \x03(\x0b\x32\x35.chromiumos.BuilderConfig.Artifacts.InputArtifactInfo\x12\x42\n\x0f\x61\x64\x64itional_args\x18\x05 \x01(\x0b\x32).chromiumos.PrepareForBuildAdditionalArgs\x12\x35\n\x0cprofile_info\x18\x06 \x01(\x0b\x32\x1f.chromiumos.ArtifactProfileInfo\"q\n PrepareForToolchainBuildResponse\x12M\n\x0f\x62uild_relevance\x18\x01 \x01(\x0e\x32\x34.chromite.api.PrepareForBuildResponse.BuildRelevance\"\xbe\x02\n\x16\x42undleToolchainRequest\x12\"\n\x06\x63hroot\x18\x01 \x01(\x0b\x32\x12.chromiumos.Chroot\x12&\n\x07sysroot\x18\x02 \x01(\x0b\x32\x15.chromite.api.Sysroot\x12\x12\n\noutput_dir\x18\x03 \x01(\t\x12I\n\x0e\x61rtifact_types\x18\x04 \x03(\x0e\x32\x31.chromiumos.BuilderConfig.Artifacts.ArtifactTypes\x12\x42\n\x0f\x61\x64\x64itional_args\x18\x05 \x01(\x0b\x32).chromiumos.PrepareForBuildAdditionalArgs\x12\x35\n\x0cprofile_info\x18\x06 \x01(\x0b\x32\x1f.chromiumos.ArtifactProfileInfo\"S\n\x17\x42undleToolchainResponse\x12\x32\n\x0e\x61rtifacts_info\x18\x02 \x03(\x0b\x32\x1a.chromite.api.ArtifactInfoJ\x04\x08\x01\x10\x02\"\x80\x01\n\x1aVerifyAFDOArtifactsRequest\x12-\n\x0c\x62uild_target\x18\x01 \x01(\x0b\x32\x17.chromiumos.BuildTarget\x12\x33\n\rartifact_type\x18\x02 \x01(\x0e\x32\x1c.chromiumos.AFDOArtifactType\"-\n\x1bVerifyAFDOArtifactsResponse\x12\x0e\n\x06status\x18\x01 \x01(\x08*f\n\x10\x41\x46\x44OArtifactType\x12\r\n\tNONE_TYPE\x10\x00\x12\r\n\tORDERFILE\x10\x01\x12\x12\n\x0e\x42\x45NCHMARK_AFDO\x10\x02\x12\x0f\n\x0bKERNEL_AFDO\x10\x03\x12\x0f\n\x0b\x43HROME_AFDO\x10\x04\x32\xef\x03\n\x10ToolchainService\x12|\n\x1dUpdateEbuildWithAFDOArtifacts\x12(.chromite.api.VerifyAFDOArtifactsRequest\x1a).chromite.api.VerifyAFDOArtifactsResponse\"\x06\xc2\xed\x1a\x02\x10\x01\x12x\n\x19UploadVettedAFDOArtifacts\x12(.chromite.api.VerifyAFDOArtifactsRequest\x1a).chromite.api.VerifyAFDOArtifactsResponse\"\x06\xc2\xed\x1a\x02\x10\x01\x12p\n\x0fPrepareForBuild\x12-.chromite.api.PrepareForToolchainBuildRequest\x1a..chromite.api.PrepareForToolchainBuildResponse\x12^\n\x0f\x42undleArtifacts\x12$.chromite.api.BundleToolchainRequest\x1a%.chromite.api.BundleToolchainResponse\x1a\x11\xc2\xed\x1a\r\n\ttoolchain\x10\x02\x42\x38Z6go.chromium.org/chromiumos/infra/proto/go/chromite/apib\x06proto3')
+  serialized_pb=_b('\n\x1c\x63hromite/api/toolchain.proto\x12\x0c\x63hromite.api\x1a\x1c\x63hromite/api/artifacts.proto\x1a\x1c\x63hromite/api/build_api.proto\x1a\x1a\x63hromite/api/sysroot.proto\x1a\x1f\x63hromiumos/builder_config.proto\x1a\x17\x63hromiumos/common.proto\"\x83\x01\n\x0c\x41rtifactInfo\x12H\n\rartifact_type\x18\x01 \x01(\x0e\x32\x31.chromiumos.BuilderConfig.Artifacts.ArtifactTypes\x12)\n\tartifacts\x18\x02 \x03(\x0b\x32\x16.chromite.api.Artifact\"\x83\x03\n\x1fPrepareForToolchainBuildRequest\x12I\n\x0e\x61rtifact_types\x18\x01 \x03(\x0e\x32\x31.chromiumos.BuilderConfig.Artifacts.ArtifactTypes\x12\"\n\x06\x63hroot\x18\x02 \x01(\x0b\x32\x12.chromiumos.Chroot\x12&\n\x07sysroot\x18\x03 \x01(\x0b\x32\x15.chromite.api.Sysroot\x12N\n\x0finput_artifacts\x18\x04 \x03(\x0b\x32\x35.chromiumos.BuilderConfig.Artifacts.InputArtifactInfo\x12\x42\n\x0f\x61\x64\x64itional_args\x18\x05 \x01(\x0b\x32).chromiumos.PrepareForBuildAdditionalArgs\x12\x35\n\x0cprofile_info\x18\x06 \x01(\x0b\x32\x1f.chromiumos.ArtifactProfileInfo\"q\n PrepareForToolchainBuildResponse\x12M\n\x0f\x62uild_relevance\x18\x01 \x01(\x0e\x32\x34.chromite.api.PrepareForBuildResponse.BuildRelevance\"\xbe\x02\n\x16\x42undleToolchainRequest\x12\"\n\x06\x63hroot\x18\x01 \x01(\x0b\x32\x12.chromiumos.Chroot\x12&\n\x07sysroot\x18\x02 \x01(\x0b\x32\x15.chromite.api.Sysroot\x12\x12\n\noutput_dir\x18\x03 \x01(\t\x12I\n\x0e\x61rtifact_types\x18\x04 \x03(\x0e\x32\x31.chromiumos.BuilderConfig.Artifacts.ArtifactTypes\x12\x42\n\x0f\x61\x64\x64itional_args\x18\x05 \x01(\x0b\x32).chromiumos.PrepareForBuildAdditionalArgs\x12\x35\n\x0cprofile_info\x18\x06 \x01(\x0b\x32\x1f.chromiumos.ArtifactProfileInfo\"S\n\x17\x42undleToolchainResponse\x12\x32\n\x0e\x61rtifacts_info\x18\x02 \x03(\x0b\x32\x1a.chromite.api.ArtifactInfoJ\x04\x08\x01\x10\x02\"\xeb\x01\n\x16GetUpdatedFilesRequest\x12R\n\x12uploaded_artifacts\x18\x01 \x03(\x0b\x32\x36.chromite.api.GetUpdatedFilesRequest.UploadedArtifacts\x1a}\n\x11UploadedArtifacts\x12\x31\n\rartifact_info\x18\x01 \x01(\x0b\x32\x1a.chromite.api.ArtifactInfo\x12\x35\n\x0cprofile_info\x18\x02 \x01(\x0b\x32\x1f.chromiumos.ArtifactProfileInfo\"\xf4\x03\n\x17GetUpdatedFilesResponse\x12H\n\rupdated_files\x18\x01 \x03(\x0b\x32\x31.chromite.api.GetUpdatedFilesResponse.UpdatedFile\x12\x16\n\x0e\x63ommit_message\x18\x02 \x01(\t\x12I\n\rcommit_footer\x18\x03 \x03(\x0b\x32\x32.chromite.api.GetUpdatedFilesResponse.CommitFooter\x1a\x1b\n\x0bUpdatedFile\x12\x0c\n\x04path\x18\x01 \x01(\t\x1a\x41\n\x0e\x43qDependFooter\x12/\n\rgerrit_change\x18\x01 \x03(\x0b\x32\x18.chromiumos.GerritChange\x1a\x1c\n\rCqClTagFooter\x12\x0b\n\x03tag\x18\x01 \x01(\t\x1a\xad\x01\n\x0c\x43ommitFooter\x12I\n\tcq_depend\x18\x01 \x01(\x0b\x32\x34.chromite.api.GetUpdatedFilesResponse.CqDependFooterH\x00\x12H\n\tcq_cl_tag\x18\x02 \x01(\x0b\x32\x33.chromite.api.GetUpdatedFilesResponse.CqClTagFooterH\x00\x42\x08\n\x06\x66ooter\"\x80\x01\n\x1aVerifyAFDOArtifactsRequest\x12-\n\x0c\x62uild_target\x18\x01 \x01(\x0b\x32\x17.chromiumos.BuildTarget\x12\x33\n\rartifact_type\x18\x02 \x01(\x0e\x32\x1c.chromiumos.AFDOArtifactType\"-\n\x1bVerifyAFDOArtifactsResponse\x12\x0e\n\x06status\x18\x01 \x01(\x08*f\n\x10\x41\x46\x44OArtifactType\x12\r\n\tNONE_TYPE\x10\x00\x12\r\n\tORDERFILE\x10\x01\x12\x12\n\x0e\x42\x45NCHMARK_AFDO\x10\x02\x12\x0f\n\x0bKERNEL_AFDO\x10\x03\x12\x0f\n\x0b\x43HROME_AFDO\x10\x04\x32\xcf\x04\n\x10ToolchainService\x12|\n\x1dUpdateEbuildWithAFDOArtifacts\x12(.chromite.api.VerifyAFDOArtifactsRequest\x1a).chromite.api.VerifyAFDOArtifactsResponse\"\x06\xc2\xed\x1a\x02\x10\x01\x12x\n\x19UploadVettedAFDOArtifacts\x12(.chromite.api.VerifyAFDOArtifactsRequest\x1a).chromite.api.VerifyAFDOArtifactsResponse\"\x06\xc2\xed\x1a\x02\x10\x01\x12p\n\x0fPrepareForBuild\x12-.chromite.api.PrepareForToolchainBuildRequest\x1a..chromite.api.PrepareForToolchainBuildResponse\x12^\n\x0f\x42undleArtifacts\x12$.chromite.api.BundleToolchainRequest\x1a%.chromite.api.BundleToolchainResponse\x12^\n\x0fGetUpdatedFiles\x12$.chromite.api.GetUpdatedFilesRequest\x1a%.chromite.api.GetUpdatedFilesResponse\x1a\x11\xc2\xed\x1a\r\n\ttoolchain\x10\x02\x42\x38Z6go.chromium.org/chromiumos/infra/proto/go/chromite/apib\x06proto3')
   ,
   dependencies=[chromite_dot_api_dot_artifacts__pb2.DESCRIPTOR,chromite_dot_api_dot_build__api__pb2.DESCRIPTOR,chromite_dot_api_dot_sysroot__pb2.DESCRIPTOR,chromiumos_dot_builder__config__pb2.DESCRIPTOR,chromiumos_dot_common__pb2.DESCRIPTOR,])
 
@@ -58,8 +58,8 @@
   ],
   containing_type=None,
   serialized_options=None,
-  serialized_start=1415,
-  serialized_end=1517,
+  serialized_start=2156,
+  serialized_end=2258,
 )
 _sym_db.RegisterEnumDescriptor(_AFDOARTIFACTTYPE)
 
@@ -304,6 +304,249 @@
 )
 
 
+_GETUPDATEDFILESREQUEST_UPLOADEDARTIFACTS = _descriptor.Descriptor(
+  name='UploadedArtifacts',
+  full_name='chromite.api.GetUpdatedFilesRequest.UploadedArtifacts',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='artifact_info', full_name='chromite.api.GetUpdatedFilesRequest.UploadedArtifacts.artifact_info', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='profile_info', full_name='chromite.api.GetUpdatedFilesRequest.UploadedArtifacts.profile_info', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1348,
+  serialized_end=1473,
+)
+
+_GETUPDATEDFILESREQUEST = _descriptor.Descriptor(
+  name='GetUpdatedFilesRequest',
+  full_name='chromite.api.GetUpdatedFilesRequest',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='uploaded_artifacts', full_name='chromite.api.GetUpdatedFilesRequest.uploaded_artifacts', index=0,
+      number=1, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[_GETUPDATEDFILESREQUEST_UPLOADEDARTIFACTS, ],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1238,
+  serialized_end=1473,
+)
+
+
+_GETUPDATEDFILESRESPONSE_UPDATEDFILE = _descriptor.Descriptor(
+  name='UpdatedFile',
+  full_name='chromite.api.GetUpdatedFilesResponse.UpdatedFile',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='path', full_name='chromite.api.GetUpdatedFilesResponse.UpdatedFile.path', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1676,
+  serialized_end=1703,
+)
+
+_GETUPDATEDFILESRESPONSE_CQDEPENDFOOTER = _descriptor.Descriptor(
+  name='CqDependFooter',
+  full_name='chromite.api.GetUpdatedFilesResponse.CqDependFooter',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='gerrit_change', full_name='chromite.api.GetUpdatedFilesResponse.CqDependFooter.gerrit_change', index=0,
+      number=1, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1705,
+  serialized_end=1770,
+)
+
+_GETUPDATEDFILESRESPONSE_CQCLTAGFOOTER = _descriptor.Descriptor(
+  name='CqClTagFooter',
+  full_name='chromite.api.GetUpdatedFilesResponse.CqClTagFooter',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='tag', full_name='chromite.api.GetUpdatedFilesResponse.CqClTagFooter.tag', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1772,
+  serialized_end=1800,
+)
+
+_GETUPDATEDFILESRESPONSE_COMMITFOOTER = _descriptor.Descriptor(
+  name='CommitFooter',
+  full_name='chromite.api.GetUpdatedFilesResponse.CommitFooter',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='cq_depend', full_name='chromite.api.GetUpdatedFilesResponse.CommitFooter.cq_depend', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='cq_cl_tag', full_name='chromite.api.GetUpdatedFilesResponse.CommitFooter.cq_cl_tag', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+    _descriptor.OneofDescriptor(
+      name='footer', full_name='chromite.api.GetUpdatedFilesResponse.CommitFooter.footer',
+      index=0, containing_type=None, fields=[]),
+  ],
+  serialized_start=1803,
+  serialized_end=1976,
+)
+
+_GETUPDATEDFILESRESPONSE = _descriptor.Descriptor(
+  name='GetUpdatedFilesResponse',
+  full_name='chromite.api.GetUpdatedFilesResponse',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='updated_files', full_name='chromite.api.GetUpdatedFilesResponse.updated_files', index=0,
+      number=1, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='commit_message', full_name='chromite.api.GetUpdatedFilesResponse.commit_message', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='commit_footer', full_name='chromite.api.GetUpdatedFilesResponse.commit_footer', index=2,
+      number=3, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[_GETUPDATEDFILESRESPONSE_UPDATEDFILE, _GETUPDATEDFILESRESPONSE_CQDEPENDFOOTER, _GETUPDATEDFILESRESPONSE_CQCLTAGFOOTER, _GETUPDATEDFILESRESPONSE_COMMITFOOTER, ],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1476,
+  serialized_end=1976,
+)
+
+
 _VERIFYAFDOARTIFACTSREQUEST = _descriptor.Descriptor(
   name='VerifyAFDOArtifactsRequest',
   full_name='chromite.api.VerifyAFDOArtifactsRequest',
@@ -337,8 +580,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1238,
-  serialized_end=1366,
+  serialized_start=1979,
+  serialized_end=2107,
 )
 
 
@@ -368,8 +611,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1368,
-  serialized_end=1413,
+  serialized_start=2109,
+  serialized_end=2154,
 )
 
 _ARTIFACTINFO.fields_by_name['artifact_type'].enum_type = chromiumos_dot_builder__config__pb2._BUILDERCONFIG_ARTIFACTS_ARTIFACTTYPES
@@ -387,6 +630,25 @@
 _BUNDLETOOLCHAINREQUEST.fields_by_name['additional_args'].message_type = chromiumos_dot_common__pb2._PREPAREFORBUILDADDITIONALARGS
 _BUNDLETOOLCHAINREQUEST.fields_by_name['profile_info'].message_type = chromiumos_dot_common__pb2._ARTIFACTPROFILEINFO
 _BUNDLETOOLCHAINRESPONSE.fields_by_name['artifacts_info'].message_type = _ARTIFACTINFO
+_GETUPDATEDFILESREQUEST_UPLOADEDARTIFACTS.fields_by_name['artifact_info'].message_type = _ARTIFACTINFO
+_GETUPDATEDFILESREQUEST_UPLOADEDARTIFACTS.fields_by_name['profile_info'].message_type = chromiumos_dot_common__pb2._ARTIFACTPROFILEINFO
+_GETUPDATEDFILESREQUEST_UPLOADEDARTIFACTS.containing_type = _GETUPDATEDFILESREQUEST
+_GETUPDATEDFILESREQUEST.fields_by_name['uploaded_artifacts'].message_type = _GETUPDATEDFILESREQUEST_UPLOADEDARTIFACTS
+_GETUPDATEDFILESRESPONSE_UPDATEDFILE.containing_type = _GETUPDATEDFILESRESPONSE
+_GETUPDATEDFILESRESPONSE_CQDEPENDFOOTER.fields_by_name['gerrit_change'].message_type = chromiumos_dot_common__pb2._GERRITCHANGE
+_GETUPDATEDFILESRESPONSE_CQDEPENDFOOTER.containing_type = _GETUPDATEDFILESRESPONSE
+_GETUPDATEDFILESRESPONSE_CQCLTAGFOOTER.containing_type = _GETUPDATEDFILESRESPONSE
+_GETUPDATEDFILESRESPONSE_COMMITFOOTER.fields_by_name['cq_depend'].message_type = _GETUPDATEDFILESRESPONSE_CQDEPENDFOOTER
+_GETUPDATEDFILESRESPONSE_COMMITFOOTER.fields_by_name['cq_cl_tag'].message_type = _GETUPDATEDFILESRESPONSE_CQCLTAGFOOTER
+_GETUPDATEDFILESRESPONSE_COMMITFOOTER.containing_type = _GETUPDATEDFILESRESPONSE
+_GETUPDATEDFILESRESPONSE_COMMITFOOTER.oneofs_by_name['footer'].fields.append(
+  _GETUPDATEDFILESRESPONSE_COMMITFOOTER.fields_by_name['cq_depend'])
+_GETUPDATEDFILESRESPONSE_COMMITFOOTER.fields_by_name['cq_depend'].containing_oneof = _GETUPDATEDFILESRESPONSE_COMMITFOOTER.oneofs_by_name['footer']
+_GETUPDATEDFILESRESPONSE_COMMITFOOTER.oneofs_by_name['footer'].fields.append(
+  _GETUPDATEDFILESRESPONSE_COMMITFOOTER.fields_by_name['cq_cl_tag'])
+_GETUPDATEDFILESRESPONSE_COMMITFOOTER.fields_by_name['cq_cl_tag'].containing_oneof = _GETUPDATEDFILESRESPONSE_COMMITFOOTER.oneofs_by_name['footer']
+_GETUPDATEDFILESRESPONSE.fields_by_name['updated_files'].message_type = _GETUPDATEDFILESRESPONSE_UPDATEDFILE
+_GETUPDATEDFILESRESPONSE.fields_by_name['commit_footer'].message_type = _GETUPDATEDFILESRESPONSE_COMMITFOOTER
 _VERIFYAFDOARTIFACTSREQUEST.fields_by_name['build_target'].message_type = chromiumos_dot_common__pb2._BUILDTARGET
 _VERIFYAFDOARTIFACTSREQUEST.fields_by_name['artifact_type'].enum_type = chromiumos_dot_common__pb2._AFDOARTIFACTTYPE
 DESCRIPTOR.message_types_by_name['ArtifactInfo'] = _ARTIFACTINFO
@@ -394,6 +656,8 @@
 DESCRIPTOR.message_types_by_name['PrepareForToolchainBuildResponse'] = _PREPAREFORTOOLCHAINBUILDRESPONSE
 DESCRIPTOR.message_types_by_name['BundleToolchainRequest'] = _BUNDLETOOLCHAINREQUEST
 DESCRIPTOR.message_types_by_name['BundleToolchainResponse'] = _BUNDLETOOLCHAINRESPONSE
+DESCRIPTOR.message_types_by_name['GetUpdatedFilesRequest'] = _GETUPDATEDFILESREQUEST
+DESCRIPTOR.message_types_by_name['GetUpdatedFilesResponse'] = _GETUPDATEDFILESRESPONSE
 DESCRIPTOR.message_types_by_name['VerifyAFDOArtifactsRequest'] = _VERIFYAFDOARTIFACTSREQUEST
 DESCRIPTOR.message_types_by_name['VerifyAFDOArtifactsResponse'] = _VERIFYAFDOARTIFACTSRESPONSE
 DESCRIPTOR.enum_types_by_name['AFDOArtifactType'] = _AFDOARTIFACTTYPE
@@ -434,6 +698,60 @@
   ))
 _sym_db.RegisterMessage(BundleToolchainResponse)
 
+GetUpdatedFilesRequest = _reflection.GeneratedProtocolMessageType('GetUpdatedFilesRequest', (_message.Message,), dict(
+
+  UploadedArtifacts = _reflection.GeneratedProtocolMessageType('UploadedArtifacts', (_message.Message,), dict(
+    DESCRIPTOR = _GETUPDATEDFILESREQUEST_UPLOADEDARTIFACTS,
+    __module__ = 'chromite.api.toolchain_pb2'
+    # @@protoc_insertion_point(class_scope:chromite.api.GetUpdatedFilesRequest.UploadedArtifacts)
+    ))
+  ,
+  DESCRIPTOR = _GETUPDATEDFILESREQUEST,
+  __module__ = 'chromite.api.toolchain_pb2'
+  # @@protoc_insertion_point(class_scope:chromite.api.GetUpdatedFilesRequest)
+  ))
+_sym_db.RegisterMessage(GetUpdatedFilesRequest)
+_sym_db.RegisterMessage(GetUpdatedFilesRequest.UploadedArtifacts)
+
+GetUpdatedFilesResponse = _reflection.GeneratedProtocolMessageType('GetUpdatedFilesResponse', (_message.Message,), dict(
+
+  UpdatedFile = _reflection.GeneratedProtocolMessageType('UpdatedFile', (_message.Message,), dict(
+    DESCRIPTOR = _GETUPDATEDFILESRESPONSE_UPDATEDFILE,
+    __module__ = 'chromite.api.toolchain_pb2'
+    # @@protoc_insertion_point(class_scope:chromite.api.GetUpdatedFilesResponse.UpdatedFile)
+    ))
+  ,
+
+  CqDependFooter = _reflection.GeneratedProtocolMessageType('CqDependFooter', (_message.Message,), dict(
+    DESCRIPTOR = _GETUPDATEDFILESRESPONSE_CQDEPENDFOOTER,
+    __module__ = 'chromite.api.toolchain_pb2'
+    # @@protoc_insertion_point(class_scope:chromite.api.GetUpdatedFilesResponse.CqDependFooter)
+    ))
+  ,
+
+  CqClTagFooter = _reflection.GeneratedProtocolMessageType('CqClTagFooter', (_message.Message,), dict(
+    DESCRIPTOR = _GETUPDATEDFILESRESPONSE_CQCLTAGFOOTER,
+    __module__ = 'chromite.api.toolchain_pb2'
+    # @@protoc_insertion_point(class_scope:chromite.api.GetUpdatedFilesResponse.CqClTagFooter)
+    ))
+  ,
+
+  CommitFooter = _reflection.GeneratedProtocolMessageType('CommitFooter', (_message.Message,), dict(
+    DESCRIPTOR = _GETUPDATEDFILESRESPONSE_COMMITFOOTER,
+    __module__ = 'chromite.api.toolchain_pb2'
+    # @@protoc_insertion_point(class_scope:chromite.api.GetUpdatedFilesResponse.CommitFooter)
+    ))
+  ,
+  DESCRIPTOR = _GETUPDATEDFILESRESPONSE,
+  __module__ = 'chromite.api.toolchain_pb2'
+  # @@protoc_insertion_point(class_scope:chromite.api.GetUpdatedFilesResponse)
+  ))
+_sym_db.RegisterMessage(GetUpdatedFilesResponse)
+_sym_db.RegisterMessage(GetUpdatedFilesResponse.UpdatedFile)
+_sym_db.RegisterMessage(GetUpdatedFilesResponse.CqDependFooter)
+_sym_db.RegisterMessage(GetUpdatedFilesResponse.CqClTagFooter)
+_sym_db.RegisterMessage(GetUpdatedFilesResponse.CommitFooter)
+
 VerifyAFDOArtifactsRequest = _reflection.GeneratedProtocolMessageType('VerifyAFDOArtifactsRequest', (_message.Message,), dict(
   DESCRIPTOR = _VERIFYAFDOARTIFACTSREQUEST,
   __module__ = 'chromite.api.toolchain_pb2'
@@ -457,8 +775,8 @@
   file=DESCRIPTOR,
   index=0,
   serialized_options=_b('\302\355\032\r\n\ttoolchain\020\002'),
-  serialized_start=1520,
-  serialized_end=2015,
+  serialized_start=2261,
+  serialized_end=2852,
   methods=[
   _descriptor.MethodDescriptor(
     name='UpdateEbuildWithAFDOArtifacts',
@@ -496,6 +814,15 @@
     output_type=_BUNDLETOOLCHAINRESPONSE,
     serialized_options=None,
   ),
+  _descriptor.MethodDescriptor(
+    name='GetUpdatedFiles',
+    full_name='chromite.api.ToolchainService.GetUpdatedFiles',
+    index=4,
+    containing_service=None,
+    input_type=_GETUPDATEDFILESREQUEST,
+    output_type=_GETUPDATEDFILESRESPONSE,
+    serialized_options=None,
+  ),
 ])
 _sym_db.RegisterServiceDescriptor(_TOOLCHAINSERVICE)
 
diff --git a/lib/toolchain_util.py b/lib/toolchain_util.py
index 6fd468b..ff69157 100644
--- a/lib/toolchain_util.py
+++ b/lib/toolchain_util.py
@@ -207,6 +207,10 @@
   """Error for BundleArtifactsHandler class."""
 
 
+class GetUpdatedFilesForCommitError(Error):
+  """Error for GetUpdatedFilesForCommit class."""
+
+
 class NoArtifactsToBundleError(Error):
   """Error for bundling empty collection of artifacts."""
 
@@ -2577,6 +2581,71 @@
       profile_info=profile_info).Bundle()
 
 
+class GetUpdatedFilesHandler(object):
+  """Find all changed files in the checkout and create a commit message."""
+
+  @staticmethod
+  def _UpdateKernelMetadata(kernel_version: str, profile_version: str):
+    """Update afdo_metadata json file"""
+    kernel_version = kernel_version.replace('.', '_')
+    json_file = os.path.join(TOOLCHAIN_UTILS_PATH, 'afdo_metadata',
+                             f'kernel_afdo_{kernel_version}.json')
+    assert os.path.exists(json_file), \
+      f'Metadata for {kernel_version} does not exist'
+    afdo_versions = json.loads(osutils.ReadFile(json_file))
+    kernel_name = f'chromeos-kernel-{kernel_version}'
+    assert kernel_name in afdo_versions, \
+      f'To update {kernel_name}, the entry should be in kernel_afdo.json'
+    old_value = afdo_versions[kernel_name]['name']
+    update_to_newer_profile = _RankValidCWPProfiles(
+        old_value) < _RankValidCWPProfiles(profile_version)
+    # This function is called after Bundle, so normally the profile is newer
+    # is guaranteed because Bundle function only runs when a new profile is
+    # needed to verify at the beginning of the builder. This check is to
+    # make sure there's no other updates happen between the start of the
+    # builder and the time of this function call.
+    assert update_to_newer_profile, (
+        f'Failed to update JSON file because {profile_version} is not '
+        f'newer than {old_value}')
+    afdo_versions[kernel_name]['name'] = profile_version
+    pformat.json(afdo_versions, fp=json_file)
+    return [json_file]
+
+  def __init__(self, artifact_type, artifact_path, profile_info):
+    self.artifact_path = artifact_path
+    self.profile_info = profile_info
+    if artifact_type == 'VerifiedKernelCwpAfdoFile':
+      self._update_func = self.UpdateKernelProfileMetadata
+    else:
+      raise GetUpdatedFilesForCommitError(
+          f'{artifact_type} has no handler in GetUpdatedFiles')
+
+  def UpdateKernelProfileMetadata(self):
+    kernel_version = self.profile_info.get('kernel_version')
+    if not kernel_version:
+      raise GetUpdatedFilesForCommitError('kernel_version not provided')
+    # The path obtained from artifact_path is the full path, containing
+    # extension, so we need to remove it here.
+    profile_version = os.path.basename(self.artifact_path).replace(
+        KERNEL_AFDO_COMPRESSION_SUFFIX, '')
+    files = self._UpdateKernelMetadata(kernel_version, profile_version)
+    commit_message = (
+        f'afdo_metadata: Publish new kernel profiles for {kernel_version}\n\n'
+        f'Update {kernel_version} to {profile_version}\n\n'
+        'Automatically generated in kernel verifier.\n\n'
+        'BUG=None\n'
+        'TEST=Verified in kernel-release-afdo-verify-orchestrator\n')
+    return files, commit_message
+
+  def Update(self):
+    return self._update_func()
+
+
+def GetUpdatedFiles(artifact_type, artifact_path, profile_info):
+  return GetUpdatedFilesHandler(artifact_type, artifact_path,
+                                profile_info).Update()
+
+
 # ###########################################################################
 #
 # TODO(crbug/1019868): delete once cbuildbot is gone.
diff --git a/lib/toolchain_util_unittest.py b/lib/toolchain_util_unittest.py
index afdcc91..c01b64b 100644
--- a/lib/toolchain_util_unittest.py
+++ b/lib/toolchain_util_unittest.py
@@ -528,7 +528,10 @@
     self.artifact_type = 'Unspecified'
     self.outdir = None
     self.afdo_tmp_path = None
-    self.profile_info = {}
+    self.kernel_version = '4_4'
+    self.profile_info = {
+        'kernel_version': self.kernel_version.replace('_', '.'),
+    }
     self.orderfile_name = (
         'chromeos-chrome-orderfile-field-78-3877.0-1567418235-'
         'benchmark-78.0.3893.0-r1.orderfile')
@@ -537,6 +540,7 @@
     self.debug_binary_name = 'chromeos-chrome-amd64-78.0.3893.0_rc-r1.debug'
     self.merged_afdo_name = (
         'chromeos-chrome-amd64-78.0.3893.0_rc-r1-merged.afdo')
+    self.kernel_name = 'R89-13638.0-1607337135'
 
     self.gen_order = self.PatchObject(
         toolchain_util.GenerateChromeOrderfile, 'Bundle', new=_Bundle)
@@ -725,6 +729,22 @@
         print_cmd=True,
     )
 
+  def testBundleVerifiedKernelCwpAfdoFile(self):
+    self.SetUpBundle('VerifiedKernelCwpAfdoFile')
+    mock_ebuild = self.PatchObject(
+        self.obj, '_GetArtifactVersionInEbuild', return_value=self.kernel_name)
+    ret = self.obj.Bundle()
+    profile_name = self.kernel_name + \
+      toolchain_util.KERNEL_AFDO_COMPRESSION_SUFFIX
+    verified_profile = os.path.join(self.outdir, profile_name)
+    self.assertEqual([verified_profile], ret)
+    mock_ebuild.assert_called_once_with(
+        f'chromeos-kernel-{self.kernel_version}', 'AFDO_PROFILE_VERSION')
+    profile_path = os.path.join(
+        self.chroot.path, self.sysroot[1:], 'usr', 'lib', 'debug', 'boot',
+        f'chromeos-kernel-{self.kernel_version}-{profile_name}')
+    self.copy2.assert_called_once_with(profile_path, verified_profile)
+
   def runToolchainBundleTest(self, artifact_path, tarball_name, input_files,
                              expected_output_files):
     """Asserts that the given artifact_path is tarred up properly.
@@ -1421,6 +1441,78 @@
       mocks.compress_file.assert_not_called()
 
 
+class GetUpdatedFilesTest(cros_test_lib.MockTempDirTestCase):
+  """Test functions in class GetUpdatedFilesForCommit."""
+
+  def setUp(self):
+    # Prepare a JSON file containing metadata
+    toolchain_util.TOOLCHAIN_UTILS_PATH = self.tempdir
+    osutils.SafeMakedirs(os.path.join(self.tempdir, 'afdo_metadata'))
+    self.json_file = os.path.join(self.tempdir,
+                                  'afdo_metadata/kernel_afdo_4_14.json')
+    self.kernel = '4.14'
+    self.kernel_name = self.kernel.replace('.', '_')
+    self.kernel_key_name = f'chromeos-kernel-{self.kernel_name}'
+    self.afdo_sorted_by_freshness = [
+        'R78-3865.0-1560000000.afdo', 'R78-3869.38-1562580965.afdo',
+        'R78-3866.0-1570000000.afdo'
+    ]
+    self.afdo_versions = {
+        self.kernel_key_name: {
+            'name': self.afdo_sorted_by_freshness[1],
+        },
+    }
+
+    with open(self.json_file, 'w') as f:
+      json.dump(self.afdo_versions, f)
+    self.artifact_path = os.path.join(
+        '/any/path/to/',
+        self.afdo_sorted_by_freshness[2] + \
+        toolchain_util.KERNEL_AFDO_COMPRESSION_SUFFIX
+    )
+    self.profile_info = {'kernel_version': self.kernel}
+
+  def testUpdateKernelMetadataFailureWithInvalidKernel(self):
+    with self.assertRaises(AssertionError) as context:
+      toolchain_util.GetUpdatedFilesHandler._UpdateKernelMetadata('3.8', None)
+    self.assertIn('does not exist', str(context.exception))
+
+  def testUpdateKernelMetadataFailureWithOlderProfile(self):
+    with self.assertRaises(AssertionError) as context:
+      toolchain_util.GetUpdatedFilesHandler._UpdateKernelMetadata(
+          self.kernel, self.afdo_sorted_by_freshness[0])
+    self.assertIn('is not newer than', str(context.exception))
+
+  def testUpdateKernelMetadataPass(self):
+    toolchain_util.GetUpdatedFilesHandler._UpdateKernelMetadata(
+        self.kernel, self.afdo_sorted_by_freshness[2])
+    # Check changes in JSON file
+    new_afdo_versions = json.loads(osutils.ReadFile(self.json_file))
+    self.assertEqual(len(self.afdo_versions), len(new_afdo_versions))
+    self.assertEqual(new_afdo_versions[self.kernel_key_name]['name'],
+                     self.afdo_sorted_by_freshness[2])
+    for k in self.afdo_versions:
+      # Make sure other fields are not changed
+      if k != self.kernel_key_name:
+        self.assertEqual(self.afdo_versions[k], new_afdo_versions[k])
+
+  def testUpdateKernelProfileMetadata(self):
+    ret_files, ret_commit = toolchain_util.GetUpdatedFiles(
+        'VerifiedKernelCwpAfdoFile', self.artifact_path, self.profile_info)
+    file_to_update = os.path.join(self.tempdir, 'afdo_metadata',
+                                  f'kernel_afdo_{self.kernel_name}.json')
+    self.assertEqual(ret_files, [file_to_update])
+    self.assertIn('Publish new kernel profiles', ret_commit)
+    self.assertIn(f'Update 4.14 to {self.afdo_sorted_by_freshness[2]}',
+                  ret_commit)
+
+  def testUpdateFailWithOtherTypes(self):
+    with self.assertRaises(
+        toolchain_util.GetUpdatedFilesForCommitError) as context:
+      toolchain_util.GetUpdatedFiles('OtherType', '', '')
+    self.assertIn('has no handler in GetUpdatedFiles', str(context.exception))
+
+
 class FindEbuildPathTest(cros_test_lib.MockTempDirTestCase):
   """Test top-level function _FindEbuildPath()."""