blob: cd0929467bdb550cdedeed6a47ddbe9fca695ca1 [file] [log] [blame]
# 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(""),
)
)