forked from mirrors/gecko-dev
384 lines
13 KiB
Python
384 lines
13 KiB
Python
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import pathlib
|
|
import shutil
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
from voluptuous import Optional
|
|
|
|
from taskgraph.actions import render_actions_json
|
|
from taskgraph.create import create_tasks
|
|
from taskgraph.generator import TaskGraphGenerator
|
|
from taskgraph.parameters import Parameters, get_version
|
|
from taskgraph.taskgraph import TaskGraph
|
|
from taskgraph.util.python_path import find_object
|
|
from taskgraph.util.schema import Schema, validate_schema
|
|
from taskgraph.util.vcs import Repository, get_repository
|
|
from taskgraph.util.yaml import load_yaml
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ARTIFACTS_DIR = Path("artifacts")
|
|
|
|
|
|
# For each project, this gives a set of parameters specific to the project.
|
|
# See `taskcluster/docs/parameters.rst` for information on parameters.
|
|
PER_PROJECT_PARAMETERS = {
|
|
# the default parameters are used for projects that do not match above.
|
|
"default": {
|
|
"target_tasks_method": "default",
|
|
}
|
|
}
|
|
|
|
|
|
try_task_config_schema_v2 = Schema(
|
|
{
|
|
Optional("parameters"): {str: object},
|
|
}
|
|
)
|
|
|
|
|
|
def full_task_graph_to_runnable_tasks(full_task_json):
|
|
runnable_tasks = {}
|
|
for label, node in full_task_json.items():
|
|
if not ("extra" in node["task"] and "treeherder" in node["task"]["extra"]):
|
|
continue
|
|
|
|
th = node["task"]["extra"]["treeherder"]
|
|
runnable_tasks[label] = {"symbol": th["symbol"]}
|
|
|
|
for i in ("groupName", "groupSymbol", "collection"):
|
|
if i in th:
|
|
runnable_tasks[label][i] = th[i]
|
|
if th.get("machine", {}).get("platform"):
|
|
runnable_tasks[label]["platform"] = th["machine"]["platform"]
|
|
return runnable_tasks
|
|
|
|
|
|
def taskgraph_decision(options, parameters=None):
|
|
"""
|
|
Run the decision task. This function implements `mach taskgraph decision`,
|
|
and is responsible for
|
|
|
|
* processing decision task command-line options into parameters
|
|
* running task-graph generation exactly the same way the other `mach
|
|
taskgraph` commands do
|
|
* generating a set of artifacts to memorialize the graph
|
|
* calling TaskCluster APIs to create the graph
|
|
"""
|
|
if options.get("verbose"):
|
|
logging.root.setLevel(logging.DEBUG)
|
|
|
|
parameters = parameters or (
|
|
lambda graph_config: get_decision_parameters(graph_config, options)
|
|
)
|
|
|
|
decision_task_id = os.environ["TASK_ID"]
|
|
|
|
# create a TaskGraphGenerator instance
|
|
tgg = TaskGraphGenerator(
|
|
root_dir=options.get("root"),
|
|
parameters=parameters,
|
|
decision_task_id=decision_task_id,
|
|
write_artifacts=True,
|
|
)
|
|
|
|
# write out the parameters used to generate this graph
|
|
write_artifact("parameters.yml", dict(**tgg.parameters))
|
|
|
|
# write out the public/actions.json file
|
|
write_artifact(
|
|
"actions.json",
|
|
render_actions_json(tgg.parameters, tgg.graph_config, decision_task_id),
|
|
)
|
|
|
|
# write out the full graph for reference
|
|
full_task_json = tgg.full_task_graph.to_json()
|
|
write_artifact("full-task-graph.json", full_task_json)
|
|
|
|
# write out the public/runnable-jobs.json file
|
|
write_artifact(
|
|
"runnable-jobs.json", full_task_graph_to_runnable_tasks(full_task_json)
|
|
)
|
|
|
|
# this is just a test to check whether the from_json() function is working
|
|
_, _ = TaskGraph.from_json(full_task_json)
|
|
|
|
# write out the target task set to allow reproducing this as input
|
|
write_artifact("target-tasks.json", list(tgg.target_task_set.tasks.keys()))
|
|
|
|
# write out the optimized task graph to describe what will actually happen,
|
|
# and the map of labels to taskids
|
|
write_artifact("task-graph.json", tgg.morphed_task_graph.to_json())
|
|
write_artifact("label-to-taskid.json", tgg.label_to_taskid)
|
|
|
|
# write out current run-task and fetch-content scripts
|
|
RUN_TASK_DIR = pathlib.Path(__file__).parent / "run-task"
|
|
shutil.copy2(RUN_TASK_DIR / "run-task", ARTIFACTS_DIR)
|
|
shutil.copy2(RUN_TASK_DIR / "fetch-content", ARTIFACTS_DIR)
|
|
|
|
# actually create the graph
|
|
create_tasks(
|
|
tgg.graph_config,
|
|
tgg.morphed_task_graph,
|
|
tgg.label_to_taskid,
|
|
tgg.parameters,
|
|
decision_task_id=decision_task_id,
|
|
)
|
|
|
|
|
|
def get_decision_parameters(graph_config, options):
|
|
"""
|
|
Load parameters from the command-line options for 'taskgraph decision'.
|
|
This also applies per-project parameters, based on the given project.
|
|
|
|
"""
|
|
parameters = {
|
|
n: options[n]
|
|
for n in [
|
|
"base_repository",
|
|
"base_ref",
|
|
"base_rev",
|
|
"head_repository",
|
|
"head_rev",
|
|
"head_ref",
|
|
"head_tag",
|
|
"project",
|
|
"pushlog_id",
|
|
"pushdate",
|
|
"repository_type",
|
|
"owner",
|
|
"level",
|
|
"target_tasks_method",
|
|
"tasks_for",
|
|
]
|
|
if n in options
|
|
}
|
|
|
|
repo_path = os.getcwd()
|
|
repo = get_repository(repo_path)
|
|
try:
|
|
commit_message = repo.get_commit_message()
|
|
except UnicodeDecodeError:
|
|
commit_message = ""
|
|
|
|
parameters["base_ref"] = _determine_more_accurate_base_ref(
|
|
repo,
|
|
candidate_base_ref=options.get("base_ref"),
|
|
head_ref=options.get("head_ref"),
|
|
base_rev=options.get("base_rev"),
|
|
)
|
|
|
|
parameters["base_rev"] = _determine_more_accurate_base_rev(
|
|
repo,
|
|
base_ref=parameters["base_ref"],
|
|
candidate_base_rev=options.get("base_rev"),
|
|
head_rev=options.get("head_rev"),
|
|
env_prefix=_get_env_prefix(graph_config),
|
|
)
|
|
|
|
# Define default filter list, as most configurations shouldn't need
|
|
# custom filters.
|
|
parameters["files_changed"] = repo.get_changed_files(
|
|
rev=parameters["head_rev"], base_rev=parameters["base_rev"]
|
|
)
|
|
parameters["filters"] = [
|
|
"target_tasks_method",
|
|
]
|
|
parameters["optimize_strategies"] = None
|
|
parameters["optimize_target_tasks"] = True
|
|
parameters["existing_tasks"] = {}
|
|
parameters["do_not_optimize"] = []
|
|
parameters["enable_always_target"] = True
|
|
parameters["build_number"] = 1
|
|
parameters["version"] = get_version(repo_path)
|
|
parameters["next_version"] = None
|
|
|
|
# owner must be an email, but sometimes (e.g., for ffxbld) it is not, in which
|
|
# case, fake it
|
|
if "@" not in parameters["owner"]:
|
|
parameters["owner"] += "@noreply.mozilla.org"
|
|
|
|
# use the pushdate as build_date if given, else use current time
|
|
parameters["build_date"] = parameters["pushdate"] or int(time.time())
|
|
# moz_build_date is the build identifier based on build_date
|
|
parameters["moz_build_date"] = time.strftime(
|
|
"%Y%m%d%H%M%S", time.gmtime(parameters["build_date"])
|
|
)
|
|
|
|
project = parameters["project"]
|
|
try:
|
|
parameters.update(PER_PROJECT_PARAMETERS[project])
|
|
except KeyError:
|
|
logger.warning(
|
|
f"using default project parameters; add {project} to "
|
|
f"PER_PROJECT_PARAMETERS in {__file__} to customize behavior "
|
|
"for this project"
|
|
)
|
|
parameters.update(PER_PROJECT_PARAMETERS["default"])
|
|
|
|
# `target_tasks_method` has higher precedence than `project` parameters
|
|
if options.get("target_tasks_method"):
|
|
parameters["target_tasks_method"] = options["target_tasks_method"]
|
|
|
|
# ..but can be overridden by the commit message: if it contains the special
|
|
# string "DONTBUILD" and this is an on-push decision task, then use the
|
|
# special 'nothing' target task method.
|
|
if "DONTBUILD" in commit_message and (
|
|
options["tasks_for"] in ("hg-push", "github-push")
|
|
):
|
|
parameters["target_tasks_method"] = "nothing"
|
|
|
|
if options.get("optimize_target_tasks") is not None:
|
|
parameters["optimize_target_tasks"] = options["optimize_target_tasks"]
|
|
|
|
if "decision-parameters" in graph_config["taskgraph"]:
|
|
find_object(graph_config["taskgraph"]["decision-parameters"])(
|
|
graph_config, parameters
|
|
)
|
|
|
|
if options.get("try_task_config_file"):
|
|
task_config_file = os.path.abspath(options.get("try_task_config_file"))
|
|
else:
|
|
# if try_task_config.json is present, load it
|
|
task_config_file = os.path.join(os.getcwd(), "try_task_config.json")
|
|
|
|
# load try settings
|
|
if ("try" in project and options["tasks_for"] == "hg-push") or options[
|
|
"tasks_for"
|
|
] == "github-pull-request":
|
|
set_try_config(parameters, task_config_file)
|
|
|
|
result = Parameters(**parameters)
|
|
result.check()
|
|
return result
|
|
|
|
|
|
def _determine_more_accurate_base_ref(repo, candidate_base_ref, head_ref, base_rev):
|
|
base_ref = candidate_base_ref
|
|
|
|
if not candidate_base_ref:
|
|
base_ref = repo.default_branch
|
|
elif candidate_base_ref == head_ref and base_rev == Repository.NULL_REVISION:
|
|
logger.info(
|
|
"base_ref and head_ref are identical but base_rev equals the null revision. "
|
|
"This is a new branch but Github didn't identify its actual base."
|
|
)
|
|
base_ref = repo.default_branch
|
|
|
|
if base_ref != candidate_base_ref:
|
|
logger.info(
|
|
f'base_ref has been reset from "{candidate_base_ref}" to "{base_ref}".'
|
|
)
|
|
|
|
return base_ref
|
|
|
|
|
|
def _determine_more_accurate_base_rev(
|
|
repo, base_ref, candidate_base_rev, head_rev, env_prefix
|
|
):
|
|
if not candidate_base_rev:
|
|
logger.info("base_rev is not set.")
|
|
base_ref_or_rev = base_ref
|
|
elif candidate_base_rev == Repository.NULL_REVISION:
|
|
logger.info("base_rev equals the null revision. This branch is a new one.")
|
|
base_ref_or_rev = base_ref
|
|
elif not repo.does_revision_exist_locally(candidate_base_rev):
|
|
logger.warning(
|
|
"base_rev does not exist locally. It is likely because the branch was force-pushed. "
|
|
"taskgraph is not able to assess how many commits were changed and assumes it is only "
|
|
f"the last one. Please set the {env_prefix.upper()}_BASE_REV environment variable "
|
|
"in the decision task and provide `--base-rev` to taskgraph."
|
|
)
|
|
base_ref_or_rev = base_ref
|
|
else:
|
|
base_ref_or_rev = candidate_base_rev
|
|
|
|
if base_ref_or_rev == base_ref:
|
|
logger.info(
|
|
f'Using base_ref "{base_ref}" to determine latest common revision...'
|
|
)
|
|
|
|
base_rev = repo.find_latest_common_revision(base_ref_or_rev, head_rev)
|
|
if base_rev != candidate_base_rev:
|
|
if base_ref_or_rev == candidate_base_rev:
|
|
logger.info("base_rev is not an ancestor of head_rev.")
|
|
|
|
logger.info(
|
|
f'base_rev has been reset from "{candidate_base_rev}" to "{base_rev}".'
|
|
)
|
|
|
|
return base_rev
|
|
|
|
|
|
def _get_env_prefix(graph_config):
|
|
repo_keys = list(graph_config["taskgraph"].get("repositories", {}).keys())
|
|
return repo_keys[0] if repo_keys else ""
|
|
|
|
|
|
def set_try_config(parameters, task_config_file):
|
|
if os.path.isfile(task_config_file):
|
|
logger.info(f"using try tasks from {task_config_file}")
|
|
with open(task_config_file) as fh:
|
|
task_config = json.load(fh)
|
|
task_config_version = task_config.pop("version")
|
|
if task_config_version == 2:
|
|
validate_schema(
|
|
try_task_config_schema_v2,
|
|
task_config,
|
|
"Invalid v2 `try_task_config.json`.",
|
|
)
|
|
parameters.update(task_config["parameters"])
|
|
return
|
|
else:
|
|
raise Exception(
|
|
f"Unknown `try_task_config.json` version: {task_config_version}"
|
|
)
|
|
|
|
|
|
def write_artifact(filename, data):
|
|
logger.info(f"writing artifact file `{filename}`")
|
|
if not os.path.isdir(ARTIFACTS_DIR):
|
|
os.mkdir(ARTIFACTS_DIR)
|
|
path = ARTIFACTS_DIR / filename
|
|
if filename.endswith(".yml"):
|
|
with open(path, "w") as f:
|
|
yaml.safe_dump(data, f, allow_unicode=True, default_flow_style=False)
|
|
elif filename.endswith(".json"):
|
|
with open(path, "w") as f:
|
|
json.dump(data, f, sort_keys=True, indent=2, separators=(",", ": "))
|
|
elif filename.endswith(".gz"):
|
|
import gzip
|
|
|
|
with gzip.open(path, "wb") as f:
|
|
f.write(json.dumps(data))
|
|
else:
|
|
raise TypeError(f"Don't know how to write to {filename}")
|
|
|
|
|
|
def read_artifact(filename):
|
|
path = ARTIFACTS_DIR / filename
|
|
if filename.endswith(".yml"):
|
|
return load_yaml(path, filename)
|
|
elif filename.endswith(".json"):
|
|
with open(path) as f:
|
|
return json.load(f)
|
|
elif filename.endswith(".gz"):
|
|
import gzip
|
|
|
|
with gzip.open(path, "rb") as f:
|
|
return json.load(f)
|
|
else:
|
|
raise TypeError(f"Don't know how to read {filename}")
|
|
|
|
|
|
def rename_artifact(src, dest):
|
|
os.rename(ARTIFACTS_DIR / src, ARTIFACTS_DIR / dest)
|