[buildbucket] Add set_output_gitiles_commit

Add a recipe function to set output gitiles commit of a build.
It is currently implemented as an output property with a dummy step, because
there is currently no better way to propagate this to kitchen.
Most likely the impl will be replaced when recipe engine speaks build proto,
but the public API will remain the same.

R=iannucci@chromium.org

Bug: 851585
Change-Id: I7477919c5e8e1d7ca2f319774af89f2ebbb9a89d
Reviewed-on: https://chromium-review.googlesource.com/c/1390771
Commit-Queue: Nodir Turakulov <nodir@chromium.org>
Reviewed-by: Andrii Shyshkalov <tandrii@chromium.org>
diff --git a/README.recipes.md b/README.recipes.md
index 22cc366..173423f 100644
--- a/README.recipes.md
+++ b/README.recipes.md
@@ -167,7 +167,7 @@
 
 A module for interacting with buildbucket.
 
-&emsp; **@property**<br>&mdash; **def [bucket\_v1](/recipe_modules/buildbucket/api.py#232)(self):**
+&emsp; **@property**<br>&mdash; **def [bucket\_v1](/recipe_modules/buildbucket/api.py#275)(self):**
 
 Returns bucket name in v1 format.
 
@@ -190,15 +190,15 @@
 the rules described in the .proto files.
 If the current build is not a buildbucket build, returned build.id is 0.
 
-&emsp; **@property**<br>&mdash; **def [build\_id](/recipe_modules/buildbucket/api.py#248)(self):**
+&emsp; **@property**<br>&mdash; **def [build\_id](/recipe_modules/buildbucket/api.py#291)(self):**
 
 DEPRECATED, use build.id instead.
 
-&emsp; **@property**<br>&mdash; **def [build\_input](/recipe_modules/buildbucket/api.py#253)(self):**
+&emsp; **@property**<br>&mdash; **def [build\_input](/recipe_modules/buildbucket/api.py#296)(self):**
 
 DEPRECATED, use build.input instead.
 
-&emsp; **@property**<br>&mdash; **def [builder\_id](/recipe_modules/buildbucket/api.py#258)(self):**
+&emsp; **@property**<br>&mdash; **def [builder\_id](/recipe_modules/buildbucket/api.py#301)(self):**
 
 Deprecated. Use build.builder instead.
 
@@ -206,9 +206,9 @@
 
 Returns builder name. Shortcut for .build.builder.builder.
 
-&mdash; **def [cancel\_build](/recipe_modules/buildbucket/api.py#202)(self, build_id, \*\*kwargs):**
+&mdash; **def [cancel\_build](/recipe_modules/buildbucket/api.py#245)(self, build_id, \*\*kwargs):**
 
-&mdash; **def [get\_build](/recipe_modules/buildbucket/api.py#205)(self, build_id, \*\*kwargs):**
+&mdash; **def [get\_build](/recipe_modules/buildbucket/api.py#248)(self, build_id, \*\*kwargs):**
 
 &emsp; **@property**<br>&mdash; **def [gitiles\_commit](/recipe_modules/buildbucket/api.py#123)(self):**
 
@@ -219,11 +219,11 @@
 
 Never returns None, but sub-fields may be empty.
 
-&emsp; **@property**<br>&mdash; **def [properties](/recipe_modules/buildbucket/api.py#243)(self):**
+&emsp; **@property**<br>&mdash; **def [properties](/recipe_modules/buildbucket/api.py#286)(self):**
 
 DEPRECATED, use build attribute instead.
 
-&mdash; **def [put](/recipe_modules/buildbucket/api.py#173)(self, builds, \*\*kwargs):**
+&mdash; **def [put](/recipe_modules/buildbucket/api.py#216)(self, builds, \*\*kwargs):**
 
 Puts a batch of builds.
 
@@ -249,6 +249,24 @@
 Args:
   host (str): buildbucket server host (e.g. 'cr-buildbucket.appspot.com').
 
+&mdash; **def [set\_output\_gitiles\_commit](/recipe_modules/buildbucket/api.py#171)(self, gitiles_commit):**
+
+Sets buildbucket.v2.Build.output.gitiles_commit field.
+
+This will tell other systems, consuming the build, what version of the code
+was actually used in this build and what is the position of this build
+relative to other builds of the same builder.
+
+Args:
+  gitiles_commit(buildbucket.common_pb2.GitilesCommit): the commit that was
+    actually checked out. Must have host, project and id.
+    ID must match r'^[0-9a-f]{40}$' (git revision).
+    If position is present, the build can be ordered along commits.
+    Position requires ref.
+    Ref, if not empty, must start with "refs/".
+
+Can be called at most once per build.
+
 &emsp; **@property**<br>&mdash; **def [tags\_for\_child\_build](/recipe_modules/buildbucket/api.py#134)(self):**
 
 A dict of tags (key -> value) derived from current (parent) build for a
diff --git a/recipe_modules/buildbucket/api.py b/recipe_modules/buildbucket/api.py
index a76e63f..8e84fd4 100644
--- a/recipe_modules/buildbucket/api.py
+++ b/recipe_modules/buildbucket/api.py
@@ -168,6 +168,49 @@
       new_tags['parent_buildername'] = str(self.build.builder.builder)
     return new_tags
 
+  def set_output_gitiles_commit(self, gitiles_commit):
+    """Sets buildbucket.v2.Build.output.gitiles_commit field.
+
+    This will tell other systems, consuming the build, what version of the code
+    was actually used in this build and what is the position of this build
+    relative to other builds of the same builder.
+
+    Args:
+      gitiles_commit(buildbucket.common_pb2.GitilesCommit): the commit that was
+        actually checked out. Must have host, project and id.
+        ID must match r'^[0-9a-f]{40}$' (git revision).
+        If position is present, the build can be ordered along commits.
+        Position requires ref.
+        Ref, if not empty, must start with "refs/".
+
+    Can be called at most once per build.
+    """
+    # Validate commit object.
+    c = gitiles_commit
+    assert isinstance(c, common_pb2.GitilesCommit), c
+
+    assert c.host
+    assert '/' not in c.host, c.host
+
+    assert c.project
+    assert not c.project.startswith('/'), c.project
+    assert not c.project.startswith('a/'), c.project
+    assert not c.project.endswith('/'), c.project
+
+    assert util.is_sha1_hex(c.id), c.id
+
+    # position is uint32
+    assert not c.position or c.ref
+
+    assert not c.ref or c.ref.startswith('refs/'), c.ref
+    assert not c.ref.endswith('/'), c.ref
+
+    # The fact that it sets a property value is an implementation detail.
+    res = self.m.step('set_output_gitiles_commit', cmd=None)
+    prop_name = '$recipe_engine/buildbucket/output_gitiles_commit'
+    res.presentation.properties[prop_name] = json_format.MessageToDict(
+        gitiles_commit)
+
   # RPCs.
 
   def put(self, builds, **kwargs):
diff --git a/recipe_modules/buildbucket/examples/full.expected/basic-ci-win.json b/recipe_modules/buildbucket/examples/full.expected/basic-ci-win.json
index ff8bf75..ffeee22 100644
--- a/recipe_modules/buildbucket/examples/full.expected/basic-ci-win.json
+++ b/recipe_modules/buildbucket/examples/full.expected/basic-ci-win.json
@@ -100,6 +100,13 @@
     ]
   },
   {
+    "cmd": [],
+    "name": "set_output_gitiles_commit",
+    "~followup_annotations": [
+      "@@@SET_BUILD_PROPERTY@$recipe_engine/buildbucket/output_gitiles_commit@{\"host\": \"chromium.googlesource.com\", \"id\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", \"position\": 42, \"project\": \"infra/infra\", \"ref\": \"refs/heads/master\"}@@@"
+    ]
+  },
+  {
     "jsonResult": null,
     "name": "$result"
   }
diff --git a/recipe_modules/buildbucket/examples/full.expected/basic-try.json b/recipe_modules/buildbucket/examples/full.expected/basic-try.json
index ef90e7e..1bd9515 100644
--- a/recipe_modules/buildbucket/examples/full.expected/basic-try.json
+++ b/recipe_modules/buildbucket/examples/full.expected/basic-try.json
@@ -100,6 +100,13 @@
     ]
   },
   {
+    "cmd": [],
+    "name": "set_output_gitiles_commit",
+    "~followup_annotations": [
+      "@@@SET_BUILD_PROPERTY@$recipe_engine/buildbucket/output_gitiles_commit@{\"host\": \"chromium.googlesource.com\", \"id\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", \"position\": 42, \"project\": \"infra/infra\", \"ref\": \"refs/heads/master\"}@@@"
+    ]
+  },
+  {
     "jsonResult": null,
     "name": "$result"
   }
diff --git a/recipe_modules/buildbucket/examples/full.py b/recipe_modules/buildbucket/examples/full.py
index 543ffce..2b86fe8 100644
--- a/recipe_modules/buildbucket/examples/full.py
+++ b/recipe_modules/buildbucket/examples/full.py
@@ -75,8 +75,17 @@
   if get_build_result.stdout['build']['status'] == 'SCHEDULED':
     api.buildbucket.cancel_build(new_job_id)
 
-  # Switching hostname for expectations coverage only.
+  # Setting values for expectations coverage only.
   api.buildbucket.set_buildbucket_host('cr-buildbucket-test.appspot.com')
+  api.buildbucket.set_output_gitiles_commit(
+    api.buildbucket.common_pb2.GitilesCommit(
+        host='chromium.googlesource.com',
+        project='infra/infra',
+        ref='refs/heads/master',
+        id='a' * 40,
+        position=42,
+    ),
+  )
 
 
 def GenTests(api):