blob: 942de87a11039ef47bd8d6cc164d273106a4f949 [file] [log] [blame]
# Copyright 2015 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.
"""
This recipe checks if a version update on branch <B> is necessary, where
'version' refers to the contents of the v8 version file (part of the v8
sources).
The recipe will:
- Commit a v8 version change to <B> with an incremented patch level if the
latest two commits point to the same version.
- Make sure that the actual HEAD of <B> is tagged with its v8 version (as
specified in the v8 version file at HEAD).
- Update a ref called <B>-lkgr to point to the latest commit that has a unique,
incremented version and that is tagged with that version.
"""
import re
from recipe_engine.post_process import DropExpectation, MustRun
DEPS = [
'depot_tools/gclient',
'depot_tools/git',
'recipe_engine/context',
'recipe_engine/file',
'recipe_engine/path',
'recipe_engine/properties',
'recipe_engine/python',
'recipe_engine/raw_io',
'recipe_engine/runtime',
'recipe_engine/service_account',
'recipe_engine/step',
'v8',
]
REPO = 'https://chromium.googlesource.com/v8/v8'
BRANCH_RE = re.compile(r'^\d+\.\d+$')
MAX_COMMIT_WAIT_RETRIES = 5
def InitClean(api):
"""Ensures a clean state of the git checkout."""
with api.context(cwd=api.path['checkout']):
api.git('checkout', '-f', 'FETCH_HEAD')
api.git('branch', '-D', 'work', ok_ret='any')
api.git('clean', '-ffd')
def Git(api, *args, **kwargs):
"""Convenience wrapper."""
with api.context(cwd=api.path['checkout']):
return api.git(
*args,
stdout=api.raw_io.output_text(),
**kwargs
).stdout
def GetCommitForRef(api, repo, ref):
result = Git(
api, 'ls-remote', repo, ref,
# Need str() to turn unicode into ascii in production.
name=str('git ls-remote %s' % ref.replace('/', '_')),
).strip()
if result:
# Extract hash if available. Otherwise keep empty string.
result = result.split()[0]
api.step.active_result.presentation.logs['ref'] = [result]
return result
def PushRef(api, repo, ref, hsh):
with api.context(cwd=api.path['checkout']):
api.git('push', repo, '+%s:%s' % (hsh, ref))
def LogStep(api, text):
api.step('log', ['echo', text])
def IncrementVersion(api, ref, latest_version, latest_version_file):
"""Increment the version on branch 'ref' to the next patch level and wait
for the committed ref to be gnumbd-ed or time out.
Args:
api: The recipe api.
ref: Ref name where to change the version, e.g.
refs/remotes/branch-heads/1.2.
latest_version: The currently latest version to be incremented.
latest_version_file: The content of the current version file.
"""
# Create a fresh work branch.
push_account = (
# TODO(sergiyb): Replace with api.service_account.default().get_email()
# when https://crbug.com/846923 is resolved.
'v8-ci-autoroll-builder@chops-service-accounts.iam.gserviceaccount.com'
if api.runtime.is_luci else 'v8-autoroll@chromium.org')
dt_path = api.path['checkout'].join('third_party', 'depot_tools')
with api.context(cwd=api.path['checkout'], env_prefixes={'PATH': [dt_path]}):
api.git('new-branch', 'work', '--upstream', ref)
api.git(
'config', 'user.name', 'V8 Autoroll', name='git config user.name',
)
api.git(
'config', 'user.email', push_account, name='git config user.email',
)
# Increment patch level and update file content.
latest_version = latest_version.with_incremented_patch()
latest_version_file = latest_version.update_version_file_blob(
latest_version_file)
# Write file to disk.
api.file.write_text(
'Increment version',
api.path['checkout'].join(api.v8.VERSION_FILE),
latest_version_file,
)
# Commit and push changes.
with api.context(cwd=api.path['checkout']):
api.git('commit', '-am', 'Version %s' % latest_version)
if api.properties.get('dry_run') or api.runtime.is_experimental:
api.step('Dry-run commit', cmd=None)
return
with api.context(cwd=api.path['checkout'], env_prefixes={'PATH': [dt_path]}):
api.git('cl', 'upload', '-f', '--bypass-hooks', '--send-mail',
'--private', '--tbrs', 'machenbach@chromium.org')
api.git('cl', 'land', '-f', '--bypass-hooks', name='git cl land')
# Function to check if commit has landed.
def has_landed():
with api.context(cwd=api.path['checkout']):
api.git('fetch', REPO, 'refs/branch-heads/*:refs/remotes/branch-heads/*')
real_latest_version = api.v8.read_version_from_ref(ref, 'committed')
return real_latest_version == latest_version
# Wait for commit to land (i.e. wait for gnumbd).
count = 1
while not has_landed():
if count == MAX_COMMIT_WAIT_RETRIES:
# This is racy. Someone other than this script might
# commit another version change right before the fetch (rarely).
# In this case, we time out and leave this commit untagged.
step_result = api.step(
'Waiting for commit timed out', cmd=None)
step_result.presentation.status = api.step.FAILURE
break
api.python.inline(
'Wait for commit',
'import time; time.sleep(%d)' % (5 * count),
)
count += 1
def RunSteps(api):
# Ensure a proper branch is specified.
branch = api.properties.get('branch')
# The luci-scheduler service specifies fully-qualified branch name, therefore
# remove the refs/branch-heads/ prefix if present.
if branch and branch.startswith('refs/branch-heads/'):
branch = branch[len('refs/branch-heads/'):]
if not branch or not BRANCH_RE.match(branch):
raise api.step.InfraFailure('A release branch must be specified.')
repo = api.properties.get('repo', REPO)
local_branch_ref = 'refs/remotes/branch-heads/%s' % branch
api.gclient.set_config('v8')
with api.context(cwd=api.path['builder_cache']):
api.gclient.checkout()
# Enforce a clean state.
InitClean(api)
# Check the last two versions.
latest_version_file = api.v8.read_version_file(local_branch_ref, 'latest')
latest_version = api.v8.version_from_file(latest_version_file)
previous_version = api.v8.read_version_from_ref(
local_branch_ref + '~1', 'previous')
# If the last two commits have the same version, we need to create a version
# increment.
if latest_version == previous_version:
IncrementVersion(
api, local_branch_ref, latest_version, latest_version_file)
elif not latest_version == previous_version.with_incremented_patch():
step_result = api.step(
'Incorrect patch levels between %s and %s' % (
previous_version, latest_version),
cmd=None,
)
step_result.presentation.status = api.step.WARNING
# Read again the current HEAD's version and check if it is tagged with it.
# If fetching the version change from above has timed out, we don't want
# to set the wrong tag.
head = Git(api, 'log', '-n1', '--format=%H', local_branch_ref).strip()
head_version = api.v8.read_version_from_ref(head, 'head')
tag = Git(api, 'describe', '--tags', head).strip()
if tag != str(head_version):
# Tag latest version.
if api.properties.get('dry_run') or api.runtime.is_experimental:
api.step('Dry-run tag %s' % head_version, cmd=None)
else:
with api.context(cwd=api.path['checkout']):
api.git('tag', str(head_version), head)
api.git('push', repo, str(head_version))
# Update lkgr ref. Both legacy and new ref location.
# TODO(machenbach): Remove legacy version after M68.
UpdateRef(api, repo, head, 'refs/heads/%s-lkgr' % branch)
UpdateRef(api, repo, head, 'refs/tags/lkgr/%s' % branch)
def UpdateRef(api, repo, head, lkgr_ref):
# Get the branch's current lkgr ref and update to HEAD.
current_lkgr = GetCommitForRef(api, repo, lkgr_ref)
# If the lkgr_ref doesn't exist, it's an empty string. In this case the push
# ref command will create it.
if head != current_lkgr:
if api.properties.get('dry_run') or api.runtime.is_experimental:
api.step('Dry-run lkgr update %s' % head, cmd=None)
else:
PushRef(api, repo, lkgr_ref, head)
else:
LogStep(api, 'There is no new lkgr.')
def GenTests(api):
hsh_old = '74882b7a8e55268d1658f83efefa1c2585cee723'
hsh_new = 'c1a7fd0c98a80c52fcf6763850d2ee1c41cfe8d6'
def stdout(step_name, text):
return api.override_step_data(
step_name, api.raw_io.stream_output(text, stream='stdout'))
def test(name, patch_level_previous, patch_level_latest,
patch_level_after_commit, current_lkgr, head, head_tag,
wait_count=0, commit_found_count=0, dry_run=False,
commit_loop_test_data=True):
test_data = (
api.test(name) +
api.properties.generic(mastername='client.v8.fyi',
buildername='Auto-tag',
branch='refs/branch-heads/3.4',
path_config='kitchen') +
api.v8.version_file(patch_level_latest, 'latest') +
api.v8.version_file(patch_level_previous, 'previous') +
api.v8.version_file(patch_level_after_commit, 'head') +
stdout('git log', head) +
stdout('git describe', head_tag) +
stdout(
'git ls-remote refs_heads_3.4-lkgr',
current_lkgr + '\trefs/heads/3.4-lkgr',
) +
stdout(
'git ls-remote refs_tags_lkgr_3.4',
current_lkgr + '\trefs/tags/lkgr/3.4',
)
)
if dry_run:
test_data += api.properties(dry_run=True)
elif commit_loop_test_data:
# Test data for the loop waiting for the version-increment commit.
for count in range(1, wait_count + 1):
test_data += api.v8.version_file(
patch_level_latest + bool(count == commit_found_count),
'committed',
count,
)
return test_data
# Test where version, the tag at HEAD and the lkgr are up-to-date.
yield test(
'same_lkgr',
patch_level_previous=2,
patch_level_latest=3,
patch_level_after_commit=3,
current_lkgr=hsh_old,
head=hsh_old,
head_tag='3.4.3.3',
)
# Requires a version update, sets a tag and updates the lkgr. After the
# version-increment commit has been found, 'git describe' doesn't find
# an accurate version tag.
yield test(
'update',
patch_level_previous=2,
patch_level_latest=2,
patch_level_after_commit=3,
current_lkgr=hsh_old,
head=hsh_new,
head_tag='3.4.3.2-sometext',
wait_count=2,
commit_found_count=2,
)
# Requires a version update, but times out waiting for gnumbd. After the
# timeout, HEAD still points to the last commit which has a consistent
# version tag.
yield test(
'update_timeout',
patch_level_previous=2,
patch_level_latest=2,
patch_level_after_commit=2,
current_lkgr=hsh_old,
head=hsh_old,
head_tag='3.4.3.2',
wait_count=MAX_COMMIT_WAIT_RETRIES,
commit_found_count=MAX_COMMIT_WAIT_RETRIES + 1,
)
# No updates required, but lkgr ref is missing, i.e. was never set. Also warn
# about an inconsistency in the patch levels.
yield test(
'missing',
patch_level_previous=1,
patch_level_latest=3,
patch_level_after_commit=3,
current_lkgr='',
head=hsh_new,
head_tag='3.4.3.3',
)
# Everything out-of-date, but dry run.
yield test(
'dry_run',
patch_level_previous=2,
patch_level_latest=2,
patch_level_after_commit=2,
current_lkgr='hsh_old',
head=hsh_new,
head_tag='3.4.3.1-sometext',
dry_run=True
)
# The bot was triggered without specifying a branch.
yield (
api.test('missing_branch') +
api.properties.generic(mastername='client.v8.fyi',
buildername='Auto-tag',
path_config='kitchen')
)
# Experimental mode.
yield (
test(
'experimental',
patch_level_previous=2,
patch_level_latest=2,
patch_level_after_commit=3,
current_lkgr=hsh_old,
head=hsh_new,
head_tag='3.4.3.2-sometext',
wait_count=2,
commit_found_count=2,
commit_loop_test_data=False
) +
api.runtime(is_luci=True, is_experimental=True) +
api.post_process(
MustRun,
'Dry-run commit',
'Dry-run tag 3.4.3.3',
'Dry-run lkgr update c1a7fd0c98a80c52fcf6763850d2ee1c41cfe8d6',
'Dry-run lkgr update c1a7fd0c98a80c52fcf6763850d2ee1c41cfe8d6 (2)') +
api.post_process(DropExpectation)
)