| # Copyright 2021 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Publish one or more releases to channels.""" |
| |
| from PB.recipes.fuchsia.release.publish import InputProperties |
| |
| from RECIPE_MODULES.fuchsia.utils import pluralize |
| |
| DEPS = [ |
| "fuchsia/git", |
| "fuchsia/release", |
| "fuchsia/sso", |
| "fuchsia/status_check", |
| "recipe_engine/cipd", |
| "recipe_engine/context", |
| "recipe_engine/json", |
| "recipe_engine/path", |
| "recipe_engine/properties", |
| "recipe_engine/python", |
| "recipe_engine/raw_io", |
| "recipe_engine/step", |
| ] |
| |
| PROPERTIES = InputProperties |
| PUBLISH_VERSION = "git_revision:02e632f480defb5b7d4eb26270ef9f6929763312" |
| |
| |
| def RunSteps(api, props): |
| with api.step.nest("check inputs"): |
| if not props.publish_cipd_tool: |
| api.python.failing_step( |
| "missing CIPD package input", |
| "`publish_cipd_tool` is required", |
| ) |
| if (props.release_version and props.target_branch) or ( |
| not props.release_version and not props.target_branch |
| ): |
| api.python.failing_step( |
| "invalid version input", |
| "must specify exactly one of `release_version` or `target_branch`", |
| ) |
| if props.target_branch and not props.remote: |
| api.python.failing_step( |
| "missing remote input", |
| "`remote` must be specified if `target_branch` is", |
| ) |
| if props.target_branch and not api.release.validate_branch(props.target_branch): |
| api.python.failing_step( |
| "invalid target branch input", |
| '`target_branch` must start with "releases/"', |
| ) |
| |
| checkout_dir = api.path["start_dir"].join("checkout") |
| https_remote = api.sso.sso_to_https(props.remote) |
| with api.step.nest("checkout"): |
| revision = api.git.checkout( |
| url=https_remote, |
| ref=props.target_branch or "refs/tags/releases/%s" % props.release_version, |
| path=checkout_dir, |
| cache=False, |
| ) |
| with api.step.nest("resolve release version"), api.context(cwd=checkout_dir): |
| release_version = props.release_version |
| if props.target_branch: |
| branch_version = api.release.ref_to_release_version(revision, checkout_dir) |
| if not branch_version: |
| api.python.failing_step( |
| "invalid target branch state", |
| "`target_branch` HEAD is not a release version", |
| ) |
| release_version = str(branch_version) |
| short_version = release_version.replace("releases/", "") |
| |
| successful_channels = [] |
| unsuccessful_channels = [] |
| for channel_info in props.channels: |
| publish_tool = api.cipd.ensure_tool( |
| str(props.publish_cipd_tool), |
| PUBLISH_VERSION, |
| ) |
| cmd = [ |
| publish_tool, |
| "release", |
| "-product-shortname", |
| channel_info.product_shortname, |
| "-version", |
| short_version, |
| "-channel", |
| channel_info.channel, |
| "-json-output", |
| api.json.output(), |
| ] |
| if props.publish_host: |
| cmd += ["-host", props.publish_host] |
| if props.rollout_pct: |
| cmd += ["-rollout-pct", props.rollout_pct] |
| if props.urgent_update: |
| cmd.append("-urgent-update") |
| step = api.step( |
| "propose release for %s %s to %s" |
| % (channel_info.product_shortname, short_version, channel_info.channel), |
| cmd, |
| infra_step=True, |
| ok_ret=(0, 1), |
| ) |
| if step.retcode: |
| step.presentation.status = api.step.EXCEPTION |
| unsuccessful_channels.append(channel_info) |
| else: |
| step.presentation.links["proposal"] = step.json.output["proposal"] |
| successful_channels.append(channel_info) |
| |
| # TODO(fxbug.dev/72426): Remove this behavior once go/fday no longer depends |
| # on it. |
| with api.step.nest("update channel branches"): |
| for channel_info in successful_channels: |
| update_channel_branch( |
| api, |
| https_remote, |
| checkout_dir, |
| channel_info.product_shortname, |
| channel_info.channel, |
| revision, |
| short_version, |
| ) |
| |
| if unsuccessful_channels: |
| api.python.infra_failing_step( |
| "failed to publish to %s" |
| % pluralize("channel", len(unsuccessful_channels)), |
| "see `propose release` stdouts for full failure details", |
| ) |
| |
| |
| def update_channel_branch( |
| api, https_remote, checkout_dir, product_shortname, channel, revision, short_version |
| ): |
| """Update channel branch under refs/heads/releases/channels/ to the given |
| revision. Note this will not create a new version, but merely keep a note of |
| the version it is equivalent to in the commit message. |
| |
| Args: |
| https_remote (str): Remote repository in HTTPS format. |
| checkout_dir (Path): Path to checkout. |
| product_shortname (str): Product shortname. |
| channel (str): Channel name. |
| revision (str): Revision corresponding to `short_version`. |
| short_version (str): Release version without "releases/" prefix. |
| """ |
| product_channel = "-".join((product_shortname, channel)) |
| channel_branch = "releases/channels/%s" % product_channel |
| with api.step.nest(product_channel), api.context( |
| cwd=checkout_dir, infra_steps=True |
| ): |
| branch_head = api.git.get_remote_branch_head( |
| https_remote, channel_branch, step_name="get channel branch HEAD" |
| ) |
| # If the channel branch exists, snap it to the current revision. |
| if branch_head: |
| api.git.fetch("origin", channel_branch) |
| api.git.raw_checkout(ref=branch_head) |
| api.git.snap_branch( |
| https_remote, |
| revision, |
| channel_branch, |
| "%s Snap to %s" % (api.release._COMMIT_MESSAGE_PREFIX, short_version), |
| path=checkout_dir, |
| # Use a unique temporary branch name to avoid collisions between |
| # snaps. |
| tmp_branch=product_channel, |
| checkout=False, |
| push=False, |
| ) |
| # Otherwise, create the channel branch with an empty commit. |
| else: |
| api.git.raw_checkout(ref=revision) |
| api.git.commit( |
| message="%s Initialize channel branch %s to %s" |
| % (api.release._COMMIT_MESSAGE_PREFIX, channel_branch, short_version), |
| allow_empty=True, |
| ) |
| api.git.push("HEAD:refs/heads/%s" % channel_branch) |
| |
| |
| def GenTests(api): |
| yield ( |
| api.status_check.test("missing_cipd_inputs", status="failure") |
| + api.properties( |
| channels=[{"product_shortname": "product", "channel": "channel"}], |
| ) |
| ) |
| yield ( |
| api.status_check.test("invalid_version_input", status="failure") |
| + api.properties( |
| channels=[{"product_shortname": "product", "channel": "channel"}], |
| publish_cipd_tool="fuchsia/infra/publish", |
| publish_cipd_version="version:1", |
| ) |
| ) |
| yield ( |
| api.status_check.test("missing_remote_input", status="failure") |
| + api.properties( |
| channels=[{"product_shortname": "product", "channel": "channel"}], |
| publish_cipd_tool="fuchsia/infra/publish", |
| publish_cipd_version="version:1", |
| target_branch="releases/branch", |
| ) |
| ) |
| yield ( |
| api.status_check.test("invalid_target_branch", status="failure") |
| + api.properties( |
| channels=[{"product_shortname": "product", "channel": "channel"}], |
| publish_cipd_tool="fuchsia/infra/publish", |
| publish_cipd_version="version:1", |
| target_branch="invalid_branch", |
| remote="sso://fuchsia/integration", |
| ) |
| ) |
| yield ( |
| api.status_check.test("invalid_target_branch_state", status="failure") |
| + api.properties( |
| channels=[{"product_shortname": "product", "channel": "channel"}], |
| publish_cipd_tool="fuchsia/infra/publish", |
| publish_cipd_version="version:1", |
| target_branch="releases/branch", |
| remote="sso://fuchsia/integration", |
| ) |
| + api.release.ref_to_release_version( |
| "", |
| nesting="resolve release version", |
| retcode=1, |
| ) |
| ) |
| yield ( |
| api.status_check.test("publish_from_target_branch") |
| + api.properties( |
| channels=[{"product_shortname": "product", "channel": "channel"}], |
| rollout_pct=100, |
| publish_cipd_tool="fuchsia/infra/publish", |
| publish_cipd_version="version:1", |
| target_branch="releases/branch", |
| remote="sso://fuchsia/integration", |
| ) |
| + api.release.ref_to_release_version( |
| "releases/1.20210101.0.1", |
| nesting="resolve release version", |
| ) |
| + api.step_data( |
| "propose release for product 1.20210101.0.1 to channel", |
| api.json.output({"proposal": "http://releases/proposal"}), |
| ) |
| ) |
| yield ( |
| api.status_check.test("publish_from_release_version") |
| + api.properties( |
| channels=[{"product_shortname": "product", "channel": "channel"}], |
| publish_cipd_tool="fuchsia/infra/publish", |
| publish_cipd_version="version:1", |
| release_version="1.20210101.0.1", |
| publish_host="publish.googleapis.com", |
| urgent_update=True, |
| ) |
| + api.step_data( |
| "propose release for product 1.20210101.0.1 to channel", |
| api.json.output({"proposal": "http://releases/proposal"}), |
| ) |
| ) |
| yield ( |
| api.status_check.test("failed_publish", status="infra_failure") |
| + api.properties( |
| channels=[ |
| {"product_shortname": "product", "channel": "old-channel"}, |
| {"product_shortname": "product", "channel": "new-channel"}, |
| ], |
| publish_cipd_tool="fuchsia/infra/publish", |
| publish_cipd_version="version:1", |
| release_version="1.20210101.0.1", |
| ) |
| + api.step_data( |
| "propose release for product 1.20210101.0.1 to old-channel", |
| retcode=1, |
| ) |
| + api.step_data( |
| "propose release for product 1.20210101.0.1 to new-channel", |
| api.json.output({"proposal": "http://releases/proposal"}), |
| ) |
| + api.step_data( |
| "update channel branches.product-new-channel.get channel branch HEAD", |
| api.raw_io.stream_output(""), |
| ) |
| ) |