Add release publish recipe

In an effort to match SLSA requirements on release workflows, moving some release logic into a recipe will allow for the logic to be triggered by tool_proxy and run on dart-internal SLSA compliant builders.

Bug: b/265318414
Change-Id: I1b145d5a32a97e4c40eec62c2f849df75b27d937
Reviewed-on: https://flutter-review.googlesource.com/c/recipes/+/38134
Commit-Queue: Jesse Seales <jseales@google.com>
Reviewed-by: Casey Hillers <chillers@google.com>
diff --git a/recipes/release/release_publish.expected/flutter-1.2-candidate.31.2.3-4.5.prebeta.json b/recipes/release/release_publish.expected/flutter-1.2-candidate.31.2.3-4.5.prebeta.json
new file mode 100644
index 0000000..4be9ded
--- /dev/null
+++ b/recipes/release/release_publish.expected/flutter-1.2-candidate.31.2.3-4.5.prebeta.json
@@ -0,0 +1,166 @@
+[
+  {
+    "cmd": [],
+    "name": "checkout release branch"
+  },
+  {
+    "cmd": [],
+    "name": "checkout release branch.Checkout flutter/flutter",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "-u",
+      "RECIPE_MODULE[depot_tools::git]/resources/git_setup.py",
+      "--path",
+      "[START_DIR]/flutter",
+      "--url",
+      "https://flutter.googlesource.com/mirrors/flutter"
+    ],
+    "name": "checkout release branch.Checkout flutter/flutter.git setup",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "fetch",
+      "origin",
+      "flutter-1.2-candidate.3",
+      "--recurse-submodules",
+      "--progress",
+      "--tags"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "env": {
+      "PATH": "RECIPE_REPO[depot_tools]:<PATH>"
+    },
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.git fetch",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "checkout",
+      "-f",
+      "FETCH_HEAD"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.git checkout",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "rev-parse",
+      "HEAD"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.read revision",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_TEXT@<br/>checked out 'deadbeef'<br/>@@@",
+      "@@@SET_BUILD_PROPERTY@got_revision@\"deadbeef\"@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "clean",
+      "-f",
+      "-d",
+      "-x"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.git clean",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "submodule",
+      "sync"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.submodule sync",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "submodule",
+      "update",
+      "--init",
+      "--recursive"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.submodule update",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "tag release"
+  },
+  {
+    "cmd": [
+      "git tag",
+      "1.2.3-4.5.pre",
+      "deadbeef"
+    ],
+    "name": "tag release.Add tag to release hash",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "push tags to upstream"
+  },
+  {
+    "cmd": [
+      "git push origin",
+      "1.2.3-4.5.pre"
+    ],
+    "name": "push tags to upstream.Push tag to origin",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "publish version"
+  },
+  {
+    "cmd": [
+      "git push origin",
+      "1.2.3-4.5.pre"
+    ],
+    "name": "publish version.Push release to refs/heads/beta",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipes/release/release_publish.expected/flutter-1.2-candidate.31.2.3-4.5.prestable.json b/recipes/release/release_publish.expected/flutter-1.2-candidate.31.2.3-4.5.prestable.json
new file mode 100644
index 0000000..e346e42
--- /dev/null
+++ b/recipes/release/release_publish.expected/flutter-1.2-candidate.31.2.3-4.5.prestable.json
@@ -0,0 +1,166 @@
+[
+  {
+    "cmd": [],
+    "name": "checkout release branch"
+  },
+  {
+    "cmd": [],
+    "name": "checkout release branch.Checkout flutter/flutter",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "-u",
+      "RECIPE_MODULE[depot_tools::git]/resources/git_setup.py",
+      "--path",
+      "[START_DIR]/flutter",
+      "--url",
+      "https://flutter.googlesource.com/mirrors/flutter"
+    ],
+    "name": "checkout release branch.Checkout flutter/flutter.git setup",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "fetch",
+      "origin",
+      "flutter-1.2-candidate.3",
+      "--recurse-submodules",
+      "--progress",
+      "--tags"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "env": {
+      "PATH": "RECIPE_REPO[depot_tools]:<PATH>"
+    },
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.git fetch",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "checkout",
+      "-f",
+      "FETCH_HEAD"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.git checkout",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "rev-parse",
+      "HEAD"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.read revision",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_TEXT@<br/>checked out 'deadbeef'<br/>@@@",
+      "@@@SET_BUILD_PROPERTY@got_revision@\"deadbeef\"@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "clean",
+      "-f",
+      "-d",
+      "-x"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.git clean",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "submodule",
+      "sync"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.submodule sync",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "submodule",
+      "update",
+      "--init",
+      "--recursive"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.submodule update",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "tag release"
+  },
+  {
+    "cmd": [
+      "git tag",
+      "1.2.3-4.5.pre",
+      "deadbeef"
+    ],
+    "name": "tag release.Add tag to release hash",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "push tags to upstream"
+  },
+  {
+    "cmd": [
+      "git push origin",
+      "1.2.3-4.5.pre"
+    ],
+    "name": "push tags to upstream.Push tag to origin",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "publish version"
+  },
+  {
+    "cmd": [
+      "git push origin",
+      "1.2.3-4.5.pre"
+    ],
+    "name": "publish version.Push release to refs/heads/stable",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipes/release/release_publish.expected/flutter-1.2-candidate.31.2.3beta.json b/recipes/release/release_publish.expected/flutter-1.2-candidate.31.2.3beta.json
new file mode 100644
index 0000000..c38a81b
--- /dev/null
+++ b/recipes/release/release_publish.expected/flutter-1.2-candidate.31.2.3beta.json
@@ -0,0 +1,166 @@
+[
+  {
+    "cmd": [],
+    "name": "checkout release branch"
+  },
+  {
+    "cmd": [],
+    "name": "checkout release branch.Checkout flutter/flutter",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "-u",
+      "RECIPE_MODULE[depot_tools::git]/resources/git_setup.py",
+      "--path",
+      "[START_DIR]/flutter",
+      "--url",
+      "https://flutter.googlesource.com/mirrors/flutter"
+    ],
+    "name": "checkout release branch.Checkout flutter/flutter.git setup",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "fetch",
+      "origin",
+      "flutter-1.2-candidate.3",
+      "--recurse-submodules",
+      "--progress",
+      "--tags"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "env": {
+      "PATH": "RECIPE_REPO[depot_tools]:<PATH>"
+    },
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.git fetch",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "checkout",
+      "-f",
+      "FETCH_HEAD"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.git checkout",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "rev-parse",
+      "HEAD"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.read revision",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_TEXT@<br/>checked out 'deadbeef'<br/>@@@",
+      "@@@SET_BUILD_PROPERTY@got_revision@\"deadbeef\"@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "clean",
+      "-f",
+      "-d",
+      "-x"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.git clean",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "submodule",
+      "sync"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.submodule sync",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "submodule",
+      "update",
+      "--init",
+      "--recursive"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.submodule update",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "tag release"
+  },
+  {
+    "cmd": [
+      "git tag",
+      "1.2.3",
+      "deadbeef"
+    ],
+    "name": "tag release.Add tag to release hash",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "push tags to upstream"
+  },
+  {
+    "cmd": [
+      "git push origin",
+      "1.2.3"
+    ],
+    "name": "push tags to upstream.Push tag to origin",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "publish version"
+  },
+  {
+    "cmd": [
+      "git push origin",
+      "1.2.3"
+    ],
+    "name": "publish version.Push release to refs/heads/beta",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipes/release/release_publish.expected/flutter-1.2-candidate.31.2.3stable.json b/recipes/release/release_publish.expected/flutter-1.2-candidate.31.2.3stable.json
new file mode 100644
index 0000000..9959fbb
--- /dev/null
+++ b/recipes/release/release_publish.expected/flutter-1.2-candidate.31.2.3stable.json
@@ -0,0 +1,166 @@
+[
+  {
+    "cmd": [],
+    "name": "checkout release branch"
+  },
+  {
+    "cmd": [],
+    "name": "checkout release branch.Checkout flutter/flutter",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python3",
+      "-u",
+      "RECIPE_MODULE[depot_tools::git]/resources/git_setup.py",
+      "--path",
+      "[START_DIR]/flutter",
+      "--url",
+      "https://flutter.googlesource.com/mirrors/flutter"
+    ],
+    "name": "checkout release branch.Checkout flutter/flutter.git setup",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "fetch",
+      "origin",
+      "flutter-1.2-candidate.3",
+      "--recurse-submodules",
+      "--progress",
+      "--tags"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "env": {
+      "PATH": "RECIPE_REPO[depot_tools]:<PATH>"
+    },
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.git fetch",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "checkout",
+      "-f",
+      "FETCH_HEAD"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.git checkout",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "rev-parse",
+      "HEAD"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.read revision",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_TEXT@<br/>checked out 'deadbeef'<br/>@@@",
+      "@@@SET_BUILD_PROPERTY@got_revision@\"deadbeef\"@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "clean",
+      "-f",
+      "-d",
+      "-x"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.git clean",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "submodule",
+      "sync"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.submodule sync",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "git",
+      "submodule",
+      "update",
+      "--init",
+      "--recursive"
+    ],
+    "cwd": "[START_DIR]/flutter",
+    "infra_step": true,
+    "name": "checkout release branch.Checkout flutter/flutter.submodule update",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "tag release"
+  },
+  {
+    "cmd": [
+      "git tag",
+      "1.2.3",
+      "deadbeef"
+    ],
+    "name": "tag release.Add tag to release hash",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "push tags to upstream"
+  },
+  {
+    "cmd": [
+      "git push origin",
+      "1.2.3"
+    ],
+    "name": "push tags to upstream.Push tag to origin",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "publish version"
+  },
+  {
+    "cmd": [
+      "git push origin",
+      "1.2.3"
+    ],
+    "name": "publish version.Push release to refs/heads/stable",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipes/release/release_publish.py b/recipes/release/release_publish.py
new file mode 100644
index 0000000..2344329
--- /dev/null
+++ b/recipes/release/release_publish.py
@@ -0,0 +1,87 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import re
+
+DEPS = [
+    'flutter/repo_util',
+    'recipe_engine/path',
+    'recipe_engine/platform',
+    'recipe_engine/properties',
+    'recipe_engine/runtime',
+    'recipe_engine/step',
+]
+
+stableTagRegex = r'^(\d+)\.(\d+)\.(\d+)$'
+betaTagRegex = r'^(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.pre$'
+
+def isValidTag(tag):
+  stable = re.search(stableTagRegex, tag)
+  development = re.search(betaTagRegex, tag)
+  return stable or development
+
+"""
+This recipe executes the tag and publishing stages of a flutter release.
+To trigger this recipe, tool proxy must be invoked with multi-party approval.
+Tool proxy information can be found at go/tool-proxy.
+Because of this configuration, the recipe is triggered manually during the
+release process.
+
+It is expected that a valid release branch, tag, and release_channel are passed
+to the recipe.
+"""
+def RunSteps(api):
+  branch = api.properties.get('branch')
+  tag = api.properties.get("tag")
+  release_channel = api.properties.get("release_channel")
+  assert branch and tag and release_channel
+
+  checkout_path = api.path['start_dir'].join('flutter')
+  git_url = api.properties.get('git_url') or 'https://flutter.googlesource.com/mirrors/flutter'
+
+  # Validate the given tag is correctly formatted for either stable or latest
+  assert isValidTag(tag)
+
+  # This recipe should only be executed on linux or mac machines to
+  # guard against Windows git issues
+  assert api.platform.is_linux or api.platform.is_mac
+
+  with api.step.nest('checkout release branch'):
+    release_git_hash = api.repo_util.checkout(
+        'flutter',
+        checkout_path=checkout_path,
+        url=git_url,
+        ref="refs/heads/%s" % branch,
+    )
+
+  with api.step.nest('tag release'):
+    step_args = ['git tag', tag, release_git_hash]
+    api.step('Add tag to release hash', step_args)
+
+  # Guard tag from being pushed on experimental runs
+  if not api.runtime.is_experimental:
+    with api.step.nest('push tags to upstream'):
+      step_args = ['git push origin', tag]
+      api.step('Push tag to origin', step_args)
+
+    with api.step.nest('publish version'):
+      api.step('Push release to refs/heads/%s' % release_channel, step_args)
+
+
+def GenTests(api):
+    for tag in ('1.2.3-4.5.pre', '1.2.3'):
+      for release_channel in ('stable', 'beta'):
+        test = api.test(
+            '%s%s%s' % (
+                'flutter-1.2-candidate.3',
+                tag,
+                release_channel
+            ), api.platform('mac', 64),
+            api.properties(
+                branch='flutter-1.2-candidate.3',
+                tag=tag,
+                release_channel=release_channel
+            ), api.repo_util.flutter_environment_data()
+        )
+        yield test