blob: c330a5cd38ecc1d7a2b007983fe2f06204f9d1c2 [file] [log] [blame]
#!/usr/bin/python -u
# Copyright 2019 The Dart project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# This script automates the steps required to fully roll a recent version of the
# Dart SDK into the Flutter engine (and can easily be extended to roll into the
# Flutter framework as well).
#
# The following steps are completed as part of the roll:
# - The Dart buildbots are queried to determine which SDK revision should be
# used. Only revisions which have finished all VM and Flutter builds will be
# considered, and revisions with < 90% of the VM bots green are ignored. See
# dart_buildbot_helper.py for more details.
# - dart_roll_helper.py is run with the chosen revision. This performs all the
# interesting parts of the roll including running tests. If the steps all
# complete successfully, dart_roll_helper.py exits with code 0. Other
# possible statuses are listed in dart_roll_utils.py, and will cause this
# script to exit with a non-zero exit code. dart_roll_helper.py can also be
# run manually to perform a Dart SDK roll with specific parameters.
# - The commit created by dart_roll_helper.py is pushed to a branch in
# flutter/engine and a pull request is created. Once all PR checks are
# complete, the PR is either merged or closed and all state created by this
# script is cleaned up.
#
# In order for this script to work, the following environment variables much be
# set:
# - GITHUB_API_KEY: A GitHub personal access token for the GitHub account to
# be used for uploading the SDK roll changes (see
# https://github.com/settings/tokens)
# - FLUTTER_HOME: the absolute path to the 'flutter' directory
# - ENGINE_HOME: the absolute path to the 'engine/src' directory
# - DART_SDK_HOME: the absolute path to the root of a Dart SDK project
# - ENGINE_FORK: the name of the GitHub fork to use (e.g., bkonyi/engine or
# flutter/engine)
#
# Finally, the following pip commands need to be run:
# - `pip install gitpython` for GitPython (git)
# - `pip install PyGithub` for PyGithub (github)
from dart_buildbot_helper import get_most_recent_green_build
from dart_roll_utils import *
from git import Repo
from github import Github, GithubException
import argparse
import atexit
import datetime
import os
import signal
import shutil
import subprocess
import sys
import time
GITHUB_STATUS_FAILURE = 'failure'
GITHUB_STATUS_PENDING = 'pending'
GITHUB_STATUS_SUCCESS = 'success'
PULL_REQUEST_DESCRIPTION = (
'This is an automated pull request which will automatically merge once '
'checks pass.'
)
FLAG_skip_wait_for_artifacts = False
CURRENT_SUBPROCESS = None
CURRENT_PR = None
GITHUB_ENGINE_REPO = None
GITHUB_ENGINE_FORK = None
def run_dart_roll_helper(most_recent_commit, extra_args):
global CURRENT_SUBPROCESS
os.environ["PYTHONUNBUFFERED"] = "1"
args = ['python',
os.path.join(os.path.dirname(__file__), 'dart_roll_helper.py'),
'--no-hot-reload',
most_recent_commit] + extra_args
CURRENT_SUBPROCESS = subprocess.Popen(args)
result = CURRENT_SUBPROCESS.wait()
CURRENT_SUBPROCESS = None
return result
# TODO(bkonyi): uncomment if we decide to roll into the framework.
# def get_engine_version_path(flutter_repo_path):
# return os.path.join(flutter_repo_path,
# 'bin',
# 'internal',
# 'engine.version')
#
#
# def get_current_engine_version(flutter_repo_path):
# with open(get_engine_version_path(flutter_repo_path), 'r') as f:
# return f.readline().strip()
#
#
# def update_engine_version(flutter_repo_path, sha):
# with open(get_engine_version_path(flutter_repo_path), 'w') as f:
# f.write(sha)
#
#
# def run_engine_roll_helper(engine_local_repo,
# flutter_repo_path,
# flutter_local_repo,
# flutter_github_repo):
# clean_and_update_repo(engine_local_repo)
# engine_commits = list(engine_local_repo.iter_commits())[:2]
# pre_roll_commit = engine_commits[1].hexsha
# roll_commit = engine_commits[0].hexsha
# current_engine_version = get_current_engine_version(flutter_repo_path)
#
# # Run `flutter doctor` until artifacts are uploaded to cloud.
# wait_for_engine_artifacts(flutter_repo_path, roll_commit)
#
# # Update the engine repo again to get any changes that may have gone in while
# # waiting for the artifacts to build.
# clean_and_update_repo(engine_local_repo)
#
# if not is_ancestor_commit(current_engine_version,
# roll_commit,
# engine_flutter_path()):
# print_status(('Existing revision {} already contains the Dart SDK roll. '
# 'No more work to do!').format(current_engine_version))
# sys.exit(ERROR_ROLL_SUCCESS)
#
# current_date = datetime.datetime.today().strftime('%Y-%m-%d')
# branch_name = 'dart-sdk-roll-{}'.format(current_date)
# engine_version_path = get_engine_version_path(flutter_repo_path)
# pr_name = 'Dart SDK roll for {}'.format(current_date)
#
# if pre_roll_commit != current_engine_version:
# # Update the engine version to the commit before the Dart SDK roll.
# # This ensures that the Dart SDK version bump is the only change in
# # the engine roll.
# update_engine_version(flutter_repo_path, pre_roll_commit)
# create_commit(flutter_local_repo,
# branch_name,
# 'Roll engine ahead of Dart SDK roll',
# [engine_version_path])
#
# # Actually update the engine version to include the Dart SDK version bump.
# update_engine_version(flutter_repo_path, roll_commit)
# create_commit(flutter_local_repo,
# branch_name,
# 'Roll engine with Dart SDK roll',
# [engine_version_path])
#
# pull_request = create_pull_request(flutter_github_repo,
# flutter_local_repo,
# pr_name,
# branch_name)
#
# merge_on_success(flutter_github_repo, pull_request)
#
#
# def wait_for_engine_artifacts(flutter_repo_path, engine_revision):
# if FLAG_skip_wait_for_artifacts:
# print_warning('Skipping wait for Flutter engine artifacts.')
# return
#
# flutter_tools = os.path.join(flutter_repo_path, 'bin', 'flutter')
# cache_path = os.path.join(flutter_repo_path, 'bin', 'cache')
#
# # Run `flutter doctor` until it can successfully find the engine artifacts
# args = [flutter_tools,
# 'doctor',
# '--check-for-remote-artifacts',
# engine_revision]
# while True:
# result = subprocess.Popen(args, stdout=subprocess.DEVNULL).wait()
# if result == 0:
# break
# time.sleep(15)
#
#
# def create_commit(local_repo, branch, message, files):
# local_repo.create_head(branch)
# local_repo.git.checkout(branch)
# index = local_repo.index
# index.add(files)
# index.commit(message)
def clean_and_update_repo(local_repo):
local_repo.git.checkout('.')
local_repo.git.clean('-xdf')
local_repo.git.checkout('master')
local_repo.git.pull()
def clean_and_update_forked_repo(local_repo):
local_repo.git.checkout('.')
local_repo.git.clean('-xdf')
local_repo.git.fetch('upstream')
local_repo.git.checkout('master')
local_repo.git.merge('upstream/master')
def clean_build_outputs():
print_status('Cleaning build directory...')
args = ['rm', '-rf',
os.path.join(ENGINE_HOME, 'out')]
CURRENT_SUBPROCESS = subprocess.Popen(args)
CURRENT_SUBPROCESS.wait()
CURRENT_SUBPROCESS = None
def delete_local_branch(local_repo, branch):
print_status('Deleting local branch {} in: {}'.format(
branch,
local_repo.working_tree_dir))
local_repo.git.checkout('master')
local_repo.delete_head(branch, '-D')
def delete_remote_branch(github_repo, branch):
print_status('Deleting remote branch on {}: {}'.format(github_repo.full_name,
branch))
github_repo.get_git_ref('heads/{}'.format(branch)).delete()
def get_most_recent_commit(local_repo):
commits = list(local_repo.iter_commits())[:1]
return commits[0]
def get_pr_title(local_repo):
commit = get_most_recent_commit(local_repo)
return commit.message.splitlines()[0].rstrip()
def create_pull_request(github_repo, local_repo, title, branch):
local_repo.create_head(branch)
local_repo.git.checkout(branch)
local_repo.git.push('origin', branch)
commit = get_most_recent_commit(local_repo)
description = PULL_REQUEST_DESCRIPTION + '\n\n' + commit.message
try:
return github_repo.create_pull(title, description,
'master', '{}:{}'.format('bkonyi', branch))
except GithubException as e:
delete_remote_branch(GITHUB_ENGINE_FORK, branch)
raise DartAutorollerException(e.data['errors'][0]['message'])
finally:
print_status('Cleaning up local branch: {}'.format(branch))
delete_local_branch(local_repo, branch)
# Remove the commit from the local master branch.
local_repo.git.reset('--hard','origin/master')
def cleanup_pr(github_repo, pull_request, reason):
msg = '{}, abandoning roll.'.format(reason)
pull_request.create_issue_comment(msg)
pull_request.edit(state='closed')
print_error(msg)
delete_remote_branch(GITHUB_ENGINE_FORK, pull_request.head.ref)
def merge_on_success(github_repo, local_repo, pull_request):
sha = pull_request.head.sha
commit = github_repo.get_commit(sha=sha)
# TODO(bkonyi): Handle case where Flutter tree is red and we're trying to
# merge into flutter/flutter.
should_merge = wait_for_status(pull_request, commit)
if should_merge:
if not pull_request.is_merged():
pull_request.create_issue_comment('Checks successful, automatically merging.')
merge_status = pull_request.merge(merge_method='squash').merged
if not merge_status:
print_error('Merge failed! Aborting roll.')
sys.exit(1)
print_status('Merge was successful!')
else:
print_status('Manual merge was performed.')
else:
cleanup_pr(github_repo, pull_request, 'Checks failed')
sys.exit(1)
delete_remote_branch(GITHUB_ENGINE_FORK, pull_request.head.ref)
# TODO(bkonyi): Check to see if the Flutter build is green for flutter/flutter
# if we decide to roll the engine into the framework.
# def flutter_build_passing(commit):
# FLUTTER_BUILD = 'flutter-build'
# statuses = commit.get_statuses()
# for status in statuses:
# if status.context == FLUTTER_BUILD:
# return (status.state == GITHUB_STATUS_SUCCESS)
# If flutter-build isn't a valid status, the PR checks don't require the
# Flutter framework to be green to submit.
# return True
# Determines if any failures are actual failures from the PR or are just
# failures from the engine builders.
def is_only_engine_build_failing(commit):
BUILD = '-build'
LUCI_ENGINE = 'luci-engine'
statuses = commit.get_statuses()
for status in statuses:
if (not ((BUILD in status.context) or
(LUCI_ENGINE == status.context)) and
(status.state == GITHUB_STATUS_FAILURE)):
return False
print_status("An engine builder is still failing...")
return True
def wait_for_status(pull_request, commit):
if FLAG_skip_wait_for_artifacts:
return True
print_status('Sleeping for 120 seconds to allow for Cirrus to start...')
# Give Cirrus a chance to start. The GitHub statuses posted by Cirrus go
# through some weird states when the PR is created and can be marked as
# failing temporarily, causing this check to return False if we don't wait.
time.sleep(120)
print_status('Starting PR status checks (this may take awhile).')
# Ensure all checks pass.
while True:
# Check to see if the PR was manually merged.
if pull_request.is_merged():
break
status = commit.get_combined_status().state
if status == GITHUB_STATUS_SUCCESS:
break
# If the only the engine builders are red, keep trying.
elif ((status == GITHUB_STATUS_FAILURE) and
(not is_only_engine_build_failing(commit))):
return False
time.sleep(5)
# TODO(bkonyi): Re-enable this check if we decide to roll the engine into the
# framework.
# Once all checks are passing, wait for the Flutter build to be green.
# while not flutter_build_passing(commit):
# print_status('Waiting for Flutter build to pass...')
# time.sleep(60)
# print_status('Flutter build passing!')
return True
def sys_exit(signal, frame):
sys.exit()
def cleanup_children():
print_error('Roll canceled! Shutting down dart_autoroller.py.')
if CURRENT_SUBPROCESS != None:
CURRENT_SUBPROCESS.terminate()
if CURRENT_PR != None:
cleanup_pr(GITHUB_ENGINE_REPO, CURRENT_PR, 'Canceled by roller')
def main():
global CURRENT_PR
global FLAG_skip_wait_for_artifacts
global GITHUB_ENGINE_REPO
global GITHUB_ENGINE_FORK
parser = argparse.ArgumentParser(description='Dart SDK autoroller for Flutter.')
parser.add_argument('--dart-sdk-revision',
help='Provide a Dart SDK revision to roll instead of '
'choosing one automatically')
parser.add_argument('--no-update-repos',
help='Skip cleaning and updating local repositories',
action='store_true')
parser.add_argument('--skip-roll',
help='Skip running dart_roll_helper.py',
action='store_true')
parser.add_argument('--skip-tests',
help='Skip running Flutter tests',
action='store_true')
parser.add_argument('--skip-build',
help='Skip building all configurations',
action='store_true')
parser.add_argument('--skip-update-deps',
help='Skip updating the Dart SDK dependencies',
action='store_true')
parser.add_argument('--skip-wait-for-artifacts',
help="Don't wait for PR statuses to pass or for engine" +
" artifacts to be uploaded to the cloud",
action='store_true', default=False)
parser.add_argument('--skip-update-licenses',
help='Skip updating the licenses for the Flutter engine',
action='store_true')
parser.add_argument('--skip-pull-request',
help="Skip creating a pull request and don't commit",
action='store_true')
args = parser.parse_args()
FLAG_skip_wait_for_artifacts = args.skip_wait_for_artifacts
github_api_key = os.getenv('GITHUB_API_KEY')
dart_sdk_path = os.getenv('DART_SDK_HOME')
flutter_path = os.getenv('FLUTTER_HOME')
engine_path = os.getenv('ENGINE_HOME')
engine_fork = os.getenv('ENGINE_FORK')
local_dart_sdk_repo = Repo(dart_sdk_path)
local_flutter_repo = Repo(flutter_path)
local_engine_flutter_repo = Repo(os.path.join(engine_path, 'flutter'))
assert(not local_dart_sdk_repo.bare)
assert(not local_flutter_repo.bare)
assert(not local_engine_flutter_repo.bare)
github = Github(github_api_key)
GITHUB_ENGINE_REPO = github.get_repo('flutter/engine')
GITHUB_ENGINE_FORK = github.get_repo(engine_fork)
github_flutter_repo = github.get_repo('flutter/flutter')
atexit.register(cleanup_children)
signal.signal(signal.SIGTERM, sys_exit)
if not args.no_update_repos:
print_status('Cleaning and updating local trees...')
clean_build_outputs()
clean_and_update_repo(local_dart_sdk_repo)
clean_and_update_repo(local_flutter_repo)
clean_and_update_forked_repo(local_engine_flutter_repo)
else:
print_warning('Skipping cleaning and updating of local trees')
# Use the most recent Dart SDK commit for the roll.
if not args.skip_roll:
print_status('Starting Dart roll helper')
most_recent_commit = ''
dart_roll_helper_args = []
if args.skip_update_deps:
dart_roll_helper_args.append('--no-update-deps')
elif args.dart_sdk_revision != None:
most_recent_commit = args.dart_sdk_revision
else:
# Get the most recent commit that is a reasonable candidate.
most_recent_commit = get_most_recent_green_build()
if args.skip_tests:
dart_roll_helper_args.append('--no-test')
if args.skip_build:
dart_roll_helper_args.append('--no-build')
if args.skip_update_licenses:
dart_roll_helper_args.append('--no-update-licenses')
if not args.skip_pull_request:
dart_roll_helper_args.append('--create-commit')
# Will exit with code ERROR_OLD_COMMIT_PROVIDED if `most_recent_commit` is
# older than the current revision of the SDK used by Flutter.
result = run_dart_roll_helper(most_recent_commit, dart_roll_helper_args)
if result != 0:
sys.exit(result)
else:
print_warning('Skipping roll step!')
if not args.skip_pull_request:
# If the local roll was successful, try to merge into the engine.
print_status('Creating flutter/engine pull request')
current_date = datetime.datetime.today().strftime('%Y-%m-%d')
try:
CURRENT_PR = create_pull_request(GITHUB_ENGINE_REPO,
local_engine_flutter_repo,
get_pr_title(local_engine_flutter_repo),
'dart-sdk-roll-{}'.format(current_date))
except DartAutorollerException as e:
print_error(('Error while creating flutter/engine pull request: {}.'
' Aborting roll.').format(e))
sys.exit(1)
print_status('Waiting for PR checks to complete...')
merge_on_success(GITHUB_ENGINE_REPO, local_engine_flutter_repo, CURRENT_PR)
print_status('PR checks complete!')
CURRENT_PR = None
else:
print_warning('Not creating flutter/engine PR!')
# TODO(bkonyi): uncomment if we decide to roll the engine into the framework.
# print_status('Starting roll of flutter/engine into flutter/flutter')
# If the roll into the engine succeeded, prep the roll into the framework.
# run_engine_roll_helper(local_engine_flutter_repo,
# flutter_path,
# local_flutter_repo,
# github_flutter_repo)
# Status code should be 0 anyway, but let's make sure our exit status is
# consistent throughout the tool on a successful roll.
sys.exit(ERROR_ROLL_SUCCESS)
if __name__ == '__main__':
main()