| # Copyright 2020 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. |
| |
| """Update project or package(s) on an integration.git release branch. |
| |
| Inputs |
| - Project: The project to update. |
| - Revision: The revision to update to. |
| |
| or |
| |
| - Packages: The package(s) to update. |
| - Version: The version to update to. |
| |
| Options |
| - Dryrun: Run recipe without modifying anything remotely. |
| """ |
| |
| from PB.go.chromium.org.luci.buildbucket.proto import build as build_pb2 |
| from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb2 |
| from PB.recipes.fuchsia.release.update_petal import InputProperties |
| |
| DEPS = [ |
| "fuchsia/checkout", |
| "fuchsia/git", |
| "fuchsia/jiri", |
| "fuchsia/release", |
| "fuchsia/sso", |
| "fuchsia/status_check", |
| "recipe_engine/context", |
| "recipe_engine/properties", |
| "recipe_engine/python", |
| "recipe_engine/raw_io", |
| "recipe_engine/step", |
| ] |
| |
| PROPERTIES = InputProperties |
| |
| COMMIT_MESSAGE = ( |
| "{prefix} Update {project_or_packages} to {revision_or_version}\n\nBug: {bug}" |
| ) |
| |
| |
| def RunSteps(api, props): |
| with api.step.nest("check inputs"): |
| if not props.target_project and not props.packages: |
| api.python.failing_step( |
| "missing project or package input", |
| "at least one of `project` or `packages` must be specified", |
| ) |
| if (props.revision and not props.target_project) or ( |
| props.target_project and not props.revision |
| ): |
| api.python.failing_step( |
| "invalid project input", |
| "`revision` and `project` must be specified together", |
| ) |
| if (props.version and not props.packages) or ( |
| props.packages and not props.version |
| ): |
| api.python.failing_step( |
| "invalid package input", |
| "`version` and `packages` must be specified together", |
| ) |
| if props.packages and not props.lockfiles: |
| api.python.failing_step( |
| "missing lockfiles input", |
| "`lockfiles` must be specified if `packages` are", |
| ) |
| if not props.bug: |
| api.python.failing_step( |
| "missing bug input", |
| "`bug` must be specified", |
| ) |
| if not api.release.validate_branch(props.target_branch): |
| api.python.failing_step( |
| "invalid target branch input", |
| '`target_branch` must start with "releases/"', |
| ) |
| # Assert target branch exists remotely. |
| https_remote = api.sso.sso_to_https(props.remote) |
| branch_revision = api.git.get_remote_branch_head( |
| step_name="get target branch HEAD", |
| url=https_remote, |
| branch=props.target_branch, |
| ) |
| if not branch_revision: |
| api.python.failing_step( |
| "target branch does not exist", |
| "target branch %s does not exist" % props.target_branch, |
| ) |
| |
| # Checkout at branch revision. |
| checkout = api.checkout.fuchsia_with_options( |
| build_input=build_pb2.Build.Input( |
| gitiles_commit=common_pb2.GitilesCommit( |
| id=branch_revision, |
| project=props.project, |
| ), |
| ), |
| project=props.project, |
| manifest=props.manifest, |
| remote=props.remote, |
| # Skip for faster checkout. |
| fetch_packages=False, |
| ) |
| integration_root = checkout.root_dir.join(props.project) |
| |
| with api.step.nest("resolve release version"): |
| # Attempt to increment candidate version. |
| release_version = api.release.get_next_candidate_version( |
| ref=branch_revision, |
| repo_path=integration_root, |
| ) |
| if api.git.get_remote_tag(url=https_remote, tag=str(release_version)): |
| api.python.failing_step( |
| "release version conflict", |
| "attempting snap would conflict with existing tag %s, " |
| "indicating branch %s has already moved forward" |
| % (release_version, props.target_branch), |
| ) |
| |
| # If updating a project, move its pin in integration to `revision`. |
| if props.target_project: |
| with api.step.nest("update project"), api.context(cwd=integration_root): |
| # The target manifest can be directed by a property. If the target |
| # manifest is not specified, use a Jiri project query to identify |
| # the target manifest. |
| target_manifest = props.target_manifest |
| if not target_manifest: |
| project_info = api.jiri.project( |
| projects=[props.target_project], |
| list_remote=True, |
| ).json.output |
| if not project_info: |
| api.python.failing_step( |
| "invalid project input", |
| "project %s does not exist in the checkout" |
| % props.target_project, |
| ) |
| target_manifest = project_info[0]["manifest"] |
| api.jiri.edit_manifest( |
| manifest=target_manifest, |
| projects=[(props.target_project, props.revision)], |
| ) |
| |
| # If updating package(s), move their pins in integration to `version` and |
| # update lockfiles. |
| else: |
| with api.step.nest("update packages"), api.context(cwd=integration_root): |
| package_info = api.jiri.package(packages=list(props.packages)).json.output |
| # Ensure we have found every package. |
| found_package_names = set() |
| found_packages = [] |
| for package in package_info: |
| # Only record latest or non-latest packages depending on |
| # `latest_packages`. |
| is_latest_manifest = package["manifest"].endswith("_latest") |
| if (props.latest_packages and is_latest_manifest) or ( |
| not props.latest_packages and not is_latest_manifest |
| ): |
| found_package_names.add(package["name"]) |
| found_packages.append(package) |
| for package_name in props.packages: |
| if package_name not in found_package_names: |
| api.python.failing_step( |
| "invalid package input", |
| "package %s does not exist in branch %s" |
| % (package_name, props.target_branch), |
| ) |
| for package in found_packages: |
| api.jiri.edit_manifest( |
| manifest=package["manifest"], |
| packages=[(package["name"], props.version)], |
| ) |
| for lock_entry in props.lockfiles: |
| fields = lock_entry.split("=") |
| manifest = fields[0] |
| lock = fields[1] |
| api.jiri.resolve( |
| local_manifest=True, |
| output=lock, |
| manifests=[manifest], |
| ) |
| |
| # Commit and push integration. |
| with api.context(cwd=integration_root), api.step.nest( |
| "update integration" |
| ) as presentation: |
| commit_message = COMMIT_MESSAGE.format( |
| prefix=api.release._COMMIT_MESSAGE_PREFIX, |
| project_or_packages=props.target_project or ", ".join(found_package_names), |
| revision_or_version=props.revision or props.version, |
| bug=props.bug, |
| ) |
| if not api.git.ls_files(step_name="check for changes", modified=True).stdout: |
| api.python.failing_step( |
| "no changes to apply", |
| "the update operation is a no-op, indicating that the target " |
| "branch already matches the requested change input", |
| ) |
| with api.context(infra_steps=True): |
| api.git.commit( |
| message=commit_message, |
| all_tracked=True, |
| ) |
| release_revision = api.git.get_hash() |
| api.git.tag(str(release_version), step_name="tag release") |
| # Push commit and release version. |
| api.git.push( |
| step_name="push release", |
| refs=[ |
| "HEAD:refs/heads/%s" % props.target_branch, |
| "refs/tags/%s" % release_version, |
| ], |
| atomic=True, |
| dryrun=props.dryrun, |
| ) |
| api.release.set_output_properties( |
| presentation, release_revision, release_version |
| ) |
| |
| if not props.dryrun and props.downstream_builders: |
| api.release.lookup_builds( |
| builders=props.downstream_builders, |
| revision=release_revision, |
| remote=https_remote, |
| ) |
| |
| |
| def GenTests(api): |
| default_revision = "foo" |
| default_target_project = "test-project" |
| default_version = "version:abc" |
| default_packages = ["fuchsia/foo/linux-amd64", "fuchsia/foo/linux-arm64"] |
| default_bug = "123456" |
| default_lockfiles = ["foo/bar=baz.lock"] |
| default_project = "integration" |
| default_manifest = "integration" |
| default_remote = "sso://fuchsia/integration" |
| default_branch = "releases/targetbranch" |
| empty_output = api.raw_io.stream_output("") |
| branch_does_not_exist = api.step_data( |
| "check inputs.get target branch HEAD", empty_output |
| ) |
| branch_version = api.release.ref_to_release_version( |
| "releases/0.20200101.0.1", |
| nesting="resolve release version", |
| ) |
| version_does_not_exist = api.step_data( |
| "resolve release version.get remote tag", empty_output |
| ) |
| project_info = api.step_data( |
| "update project.jiri project", |
| api.jiri.package( |
| [ |
| { |
| "name": default_target_project, |
| "manifest": "fuchsia/manifest", |
| "path": "path/to/project", |
| "gerrithost": "https://fuchsia-review.googlesource.com", |
| }, |
| ] |
| ), |
| ) |
| modified_files = api.step_data( |
| "update integration.check for changes", |
| api.raw_io.stream_output("foo/bar"), |
| ) |
| no_modified_files = api.step_data( |
| "update integration.check for changes", empty_output |
| ) |
| package_info = api.step_data( |
| "update packages.jiri package", |
| api.jiri.package( |
| [ |
| { |
| "name": "fuchsia/foo/linux-amd64", |
| "path": "path/linux-amd64", |
| "manifest": "test/manifest", |
| }, |
| { |
| "name": "fuchsia/foo/linux-arm64", |
| "path": "path/linux-arm64", |
| "manifest": "test/manifest", |
| }, |
| { |
| "name": "fuchsia/foo/linux-amd64", |
| "path": "path/linux-amd64", |
| "manifest": "test/manifest_latest", |
| }, |
| { |
| "name": "fuchsia/foo/linux-arm64", |
| "path": "path/linux-arm64", |
| "manifest": "test/manifest_latest", |
| }, |
| ] |
| ), |
| ) |
| |
| yield (api.status_check.test("missing_project_or_package_input", status="failure")) |
| yield ( |
| api.status_check.test("missing_revision_input", status="failure") |
| + api.properties(target_project=default_target_project) |
| ) |
| yield ( |
| api.status_check.test("missing_project_input", status="failure") |
| + api.properties(revision=default_revision) |
| ) |
| yield ( |
| api.status_check.test("missing_version_input", status="failure") |
| + api.properties(packages=default_packages) |
| ) |
| yield ( |
| api.status_check.test("missing_packages_input", status="failure") |
| + api.properties(revision=default_version) |
| ) |
| yield ( |
| api.status_check.test("missing_lockfiles_input", status="failure") |
| + api.properties(packages=default_packages, version=default_version) |
| ) |
| yield ( |
| api.status_check.test("missing_bug_input", status="failure") |
| + api.properties( |
| packages=default_packages, |
| version=default_version, |
| lockfiles=default_lockfiles, |
| ) |
| ) |
| yield ( |
| api.status_check.test("invalid_branch_input", status="failure") |
| + api.properties( |
| packages=default_packages, |
| version=default_version, |
| lockfiles=default_lockfiles, |
| bug=default_bug, |
| target_branch="invalidbranch", |
| ) |
| ) |
| yield ( |
| api.status_check.test("branch_does_not_exist", status="failure") |
| + api.properties( |
| packages=default_packages, |
| version=default_version, |
| lockfiles=default_lockfiles, |
| bug=default_bug, |
| target_branch=default_branch, |
| ) |
| + branch_does_not_exist |
| ) |
| yield ( |
| api.status_check.test("release_version_conflict", status="failure") |
| + api.properties( |
| packages=default_packages, |
| version=default_version, |
| lockfiles=default_lockfiles, |
| bug=default_bug, |
| target_branch=default_branch, |
| project=default_project, |
| manifest=default_manifest, |
| remote=default_remote, |
| ) |
| + branch_version |
| ) |
| yield ( |
| api.status_check.test("invalid_project_input", status="failure") |
| + api.properties( |
| target_project=default_target_project, |
| revision=default_revision, |
| bug=default_bug, |
| target_branch=default_branch, |
| project=default_project, |
| manifest=default_manifest, |
| remote=default_remote, |
| ) |
| + branch_version |
| + version_does_not_exist |
| + api.step_data("update project.jiri project", api.jiri.project([])) |
| ) |
| yield ( |
| api.status_check.test("update_project") |
| + api.properties( |
| target_project=default_target_project, |
| revision=default_revision, |
| bug=default_bug, |
| target_branch=default_branch, |
| project=default_project, |
| manifest=default_manifest, |
| remote=default_remote, |
| downstream_builders=["a/b/c"], |
| ) |
| + branch_version |
| + version_does_not_exist |
| + project_info |
| + modified_files |
| ) |
| yield ( |
| api.status_check.test("invalid_package_input", status="failure") |
| + api.properties( |
| packages=["invalid/package"], |
| version=default_version, |
| lockfiles=default_lockfiles, |
| bug=default_bug, |
| target_branch=default_branch, |
| project=default_project, |
| manifest=default_manifest, |
| remote=default_remote, |
| ) |
| + branch_version |
| + version_does_not_exist |
| + package_info |
| ) |
| yield ( |
| api.status_check.test("update_packages") |
| + api.properties( |
| packages=default_packages, |
| version=default_version, |
| lockfiles=default_lockfiles, |
| bug=default_bug, |
| target_branch=default_branch, |
| project=default_project, |
| manifest=default_manifest, |
| remote=default_remote, |
| ) |
| + branch_version |
| + version_does_not_exist |
| + package_info |
| + modified_files |
| ) |
| yield ( |
| api.status_check.test("no_changes_to_apply", status="failure") |
| + api.properties( |
| packages=default_packages, |
| version=default_version, |
| lockfiles=default_lockfiles, |
| bug=default_bug, |
| target_branch=default_branch, |
| project=default_project, |
| manifest=default_manifest, |
| remote=default_remote, |
| ) |
| + branch_version |
| + version_does_not_exist |
| + package_info |
| + no_modified_files |
| ) |
| yield ( |
| api.status_check.test("update_latest_packages") |
| + api.properties( |
| packages=default_packages, |
| version=default_version, |
| lockfiles=default_lockfiles, |
| bug=default_bug, |
| target_branch=default_branch, |
| project=default_project, |
| manifest=default_manifest, |
| remote=default_remote, |
| latest_packages=True, |
| ) |
| + branch_version |
| + version_does_not_exist |
| + package_info |
| + modified_files |
| ) |