Skip to content
This repository has been archived by the owner. It is now read-only.
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
executable file 495 lines (417 sloc) 18.1 KB
import logging
import sys
import urllib
from argparse import ArgumentParser
from buildapi_client import make_retrigger_request
from mozci import BuildAPIManager, TaskClusterBuildbotManager
from mozci.mozci import (
find_backfill_revlist,
manual_backfill,
query_builders,
query_repo_name_from_buildername,
query_repo_url_from_buildername,
set_query_source,
trigger_all_talos_jobs,
trigger_talos_jobs_for_build,
)
from mozci.platforms import filter_buildernames
from mozci.query_jobs import (
COALESCED,
SUCCESS,
WARNING,
BuildApi,
TreeherderApi,
)
from mozci.repositories import query_repo_url
from mozci.utils.authentication import valid_credentials, get_credentials
from mozci.utils.log_util import setup_logging
from mozhginfo.pushlog_client import (
query_pushes_by_specified_revision_range,
query_pushes_by_revision_range,
query_push_by_revision,
query_repo_tip
)
ACTIONS = {
'trigger-all-talos': {
'help': 'This will trigger all talos jobs for a revision. This will also '
'trigger the builds that the talos jobs depend on.'
},
}
def parse_args(argv=None):
"""Parse command line options."""
parser = ArgumentParser()
# XXX: Temporary change
parser.add_argument('--action',
help='Available actions: ' + ','.join(ACTIONS.keys()))
# Required arguments
parser.add_argument('-b', "--buildername",
dest="buildernames",
type=str,
help="Comma-separated buildernames used in Treeherder.")
parser.add_argument("-r", "--revision",
dest="rev",
required=True,
help='The 12 character representing a revision (most recent).')
# Optional arguments
parser.add_argument("--times",
dest="times",
type=int,
default=1,
help="Total number of jobs to have on a push. Eg: If there is\
1 job and you want to trigger 1 more time, do --times=2.")
parser.add_argument("--skips",
dest="skips",
type=int,
help="Specify the step size to skip after every retrigger.")
parser.add_argument('--from-rev',
dest='from_rev',
help='The 12 character representing the oldest push to start from.')
parser.add_argument("--max-revisions",
dest="max_revisions",
default=20,
type=int,
help="This flag is used with --backfill. This flag limits"
"how many revisions we will look back until we find"
"the last revision where there was a good job.")
parser.add_argument("--dry-run",
action="store_true",
dest="dry_run",
help="flag to test without actual push.")
parser.add_argument("--debug",
action="store_true",
dest="debug",
help="set debug for logging.")
parser.add_argument("--query-source",
metavar="[buildapi|treeherder]",
dest="query_source",
default="buildapi",
help="Query info from buildapi or treeherder.")
parser.add_argument("--file",
action="append",
dest="files",
help="Set files (typically an installer and test zip url "
"to be used in triggered jobs.")
parser.add_argument("--repo-name",
dest="repo_name",
help="Branch name")
parser.add_argument("--trigger-build-if-missing",
action="store_false",
dest="trigger_build_if_missing",
help="Only trigger test jobs if the build jobs already exists.")
parser.add_argument("--taskcluster",
action="store_true",
help="Schedule jobs through TaskCluster.")
# Mode #1: Coalesced jobs of a revision
parser.add_argument("--coalesced",
action="store_true",
dest="coalesced",
help="Trigger every coalesced job on revision --rev "
"and repo --repo-name.")
# Mode #2: Add all missing jobs for a revision
parser.add_argument("--fill-revision",
action="store_true",
dest="fill_revision",
help="Add all missing jobs to a revision.")
# Mode #3: Trigger jobs and 3 modifiers of the list of revisions to trigger on
parser.add_argument("--delta",
dest="delta",
type=int,
help="Number of jobs to add/subtract from push revision.")
parser.add_argument("--back-revisions",
dest="back_revisions",
type=int,
help="Number of revisions to go back from current revision (--rev).")
parser.add_argument("--backfill",
action="store_true",
dest="backfill",
help="We will trigger jobs starting from --rev in reverse chronological "
"order until we find the last revision where there was a good job.")
parser.add_argument("--trigger-only-test-jobs",
action="store_true",
dest="trigger_tests_only",
help="Schedule all missing tests for existing builds.")
# Mode 4: Use --includes and --exclude flags to filter multiple buildernames.
parser.add_argument('--i', "--includes",
dest="includes",
required=False,
type=str,
help="comma-separated treeherder filters to include.")
parser.add_argument('--e', "--exclude",
dest="exclude",
type=str,
help="comma-separated treeherder filters to exclude.")
parser.add_argument("--existing-only",
action="store_true",
dest="existing_only",
help="Only trigger test job if the build jobs already exists.")
# Mode 5: Use --failed-jobs to trigger jobs for particular revision
parser.add_argument('--failed-jobs',
action="store_true",
dest="failed_jobs",
help="trigger failed jobs for particular revision")
# Mode 6: Use --trigger-talos-for-build to trigger talos jobs for a particular build
parser.add_argument('--trigger-talos-for-build',
action="store_true",
dest="trigger_talos_for_build",
help="trigger all talos jobs for a particular build")
options = parser.parse_args(argv)
return options
def validate_options(options):
"""
Raises an exception if options are missing or conflicting.
"""
error_message = ""
if not(options.buildernames or options.coalesced or options.fill_revision or
options.trigger_tests_only or options.includes or options.exclude or
options.existing_only or options.failed_jobs):
error_message = "A buildername is mandatory for all modes except --coalesced, " \
"--fill-revision, --trigger-only-test-jobs --include, --exclude" \
" and --failed-jobs. Use --buildername."
if options.coalesced and not options.repo_name:
error_message = "A branch name is mandatory with --coalesced. Use --repo-name."
if options.back_revisions:
if options.backfill or options.delta or options.from_rev:
error_message = "You should not pass --backfill, --delta or --end-rev " \
"when you use --back-revisions."
elif options.backfill:
if options.delta or options.from_rev:
error_message = "You should not pass --delta or --end-rev " \
"when you use --backfill."
elif options.delta:
if options.from_rev:
error_message = "You should not pass --end-rev " \
"when you use --delta."
elif options.trigger_tests_only:
if not options.repo_name:
error_message = "A branch name is mandatory with --trigger-only-test-jobs. "\
"Use --repo-name."
if options.fill_revision:
error_message = "You should not pass --fill-revision " \
"when you use --trigger-only-test-jobs"
if options.exclude or options.includes:
if not options.repo_name:
error_message = "A repo_name is mandatory with --exclude or --include. "\
"Use --repo-name."
if options.failed_jobs:
if not options.repo_name:
error_message = "A repo_name is mandatory with --existing-jobs or " \
"--failed-job. Use --repo-name."
if error_message:
raise Exception(error_message)
def sanitize_buildernames(buildernames):
"""Return the list of buildernames without trailing spaces and with the right capitalization."""
buildernames_list = buildernames.split(',')
repo_name = set(map(query_repo_name_from_buildername, buildernames_list))
assert len(repo_name) == 1, "We only allow multiple buildernames on the same branch."
ret_value = []
for buildername in buildernames_list:
buildername = buildername.strip()
builders = query_builders()
for builder in builders:
if buildername.lower() == builder.lower():
buildername = builder
ret_value.append(buildername)
return ret_value
def determine_revlist(repo_url, buildername, rev, back_revisions,
delta, from_rev, backfill, skips, max_revisions):
"""Determine which revisions we need to trigger."""
if back_revisions:
revlist = query_pushes_by_specified_revision_range(
repo_url=repo_url,
revision=rev,
before=back_revisions,
after=0,
return_revision_list=True)
elif delta:
revlist = query_pushes_by_specified_revision_range(
repo_url=repo_url,
revision=rev,
before=delta,
after=delta,
return_revision_list=True)
elif from_rev:
revlist = query_pushes_by_revision_range(
repo_url=repo_url,
to_revision=rev,
from_revision=from_rev,
return_revision_list=True)
elif backfill:
revlist = find_backfill_revlist(
buildername=buildername,
revision=rev,
max_pushes=max_revisions,
)
else:
revlist = [rev]
if skips:
revlist = revlist[::skips]
return revlist
def _print_treeherder_link(revlist, repo_name, buildername, revision, log,
includes=False, exclude=False):
if revlist:
if includes or exclude:
log.info('https://treeherder.mozilla.org/#/jobs?%s' %
urllib.urlencode({'repo': repo_name,
'fromchange': revlist[-1],
'tochange': revlist[0],
'filter-searchStr': buildername}))
else:
log.info('https://treeherder.mozilla.org/#/jobs?%s' %
urllib.urlencode({'repo': repo_name,
'revision': revision}))
def _includes_excludes(options, repo_name):
filters_in = options.includes.split(',') + [repo_name]
filters_out = []
if options.exclude:
filters_out = options.exclude.split(',')
job_names = filter_buildernames(
buildernames=query_builders(repo_name=repo_name),
include=filters_in,
exclude=filters_out
)
if len(job_names) == 0:
LOG.info("0 jobs match these filters. please try again.")
return
if options.existing_only:
# We query all successful jobs for a given revision and filter
# them by include/exclude filters.
trigger_build_if_missing = False
successful_jobs = TreeherderApi().find_all_jobs_by_status(
repo_name=repo_name,
revision=revision,
status=SUCCESS)
# We will filter out all the existing job from those successful job we have.
job_names = [buildername for buildername in successful_jobs
if buildername in job_names]
cont = raw_input("The ones which have existing builds out of %i jobs will be triggered,\
do you wish to continue? y/n/d (d=show details) " % len(job_names))
else:
cont = raw_input("%i jobs will be triggered, do you wish to continue? \
y/n/d (d=show details) " % len(job_names))
if cont.lower() == 'd':
LOG.info("The following jobs will be triggered: \n %s" % '\n'.join(job_names))
cont = raw_input("Do you wish to continue? y/n ")
if cont.lower() != 'y':
exit(1)
def main():
options = parse_args()
if options.debug:
LOG = setup_logging(logging.DEBUG)
else:
LOG = setup_logging(logging.INFO)
if options.action == 'trigger-all-talos':
trigger_all_talos_jobs(options.repo_name, options.rev, options.times,
dry_run=options.dry_run)
sys.exit(0)
validate_options(options)
if not options.dry_run and not valid_credentials():
sys.exit(-1)
# Setting the QUERY_SOURCE global variable in mozci.py
set_query_source(options.query_source)
if options.buildernames:
options.buildernames = sanitize_buildernames(options.buildernames)
repo_url = query_repo_url_from_buildername(options.buildernames[0])
if not options.repo_name:
repo_name = query_repo_name_from_buildername(options.buildernames[0])
else:
repo_name = options.repo_name
repo_url = query_repo_url(repo_name)
if options.rev == 'tip':
revision = query_repo_tip(repo_url).changesets[0].node
LOG.info("The tip of %s is %s", repo_name, revision)
else:
revision = query_push_by_revision(repo_url, options.rev,
return_revision_list=True)
# Schedule jobs through TaskCluster if --taskcluster option has been set to true
if options.taskcluster:
mgr = TaskClusterBuildbotManager(web_auth=True)
else:
mgr = BuildAPIManager()
trigger_build_if_missing = options.trigger_build_if_missing
if repo_name == 'try':
trigger_build_if_missing = False
# Mode 0: Backfill
if options.backfill:
manual_backfill(revision, options.buildernames[0], dry_run=options.dry_run)
return
# Mode 1: Trigger coalesced jobs
if options.coalesced:
query_api = BuildApi()
request_ids = query_api.find_all_jobs_by_status(repo_name,
revision, COALESCED)
if len(request_ids) == 0:
LOG.info('We did not find any coalesced job')
for request_id in request_ids:
make_retrigger_request(repo_name=repo_name,
request_id=request_id,
auth=get_credentials(),
dry_run=options.dry_run)
return
# Mode #2: Fill-in a revision or trigger_test_jobs_only
if options.fill_revision or options.trigger_tests_only:
mgr.trigger_missing_jobs_for_revision(
repo_name=repo_name,
revision=revision,
dry_run=options.dry_run,
trigger_build_if_missing=not options.trigger_tests_only
)
return
# Mode #3: Trigger jobs based on revision list modifiers
if not (options.includes or options.exclude or options.failed_jobs or
options.trigger_talos_for_build):
job_names = options.buildernames
# Mode 4 - Schedule every builder matching --includes and does not match --exclude.
elif options.includes or options.exclude:
_includes_excludes(options)
# Mode 5: Use --failed-jobs to trigger jobs for particular revision
elif options.failed_jobs:
job_names = TreeherderApi().find_all_jobs_by_status(
repo_name=repo_name,
revision=revision,
status=WARNING)
elif options.trigger_talos_for_build:
trigger_talos_jobs_for_build(
buildername=options.buildernames[0],
revision=revision,
times=2,
dry_run=options.dry_run,
)
exit(0)
for buildername in job_names:
revlist = determine_revlist(
repo_url=repo_url,
buildername=buildername,
rev=revision,
back_revisions=options.back_revisions,
delta=options.delta,
from_rev=options.from_rev,
backfill=options.backfill,
skips=options.skips,
max_revisions=options.max_revisions)
_print_treeherder_link(
revlist=revlist,
repo_name=repo_name,
buildername=buildername,
revision=revision,
log=LOG,
includes=options.includes,
exclude=options.exclude)
try:
mgr.trigger_range(
buildername=buildername,
repo_name=repo_name,
revisions=revlist,
times=options.times,
dry_run=options.dry_run,
files=options.files,
trigger_build_if_missing=trigger_build_if_missing
)
except Exception as e:
LOG.exception(e)
exit(1)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
sys.tracebacklimit = 0
sys.exit(0)