blob: 71c25e0460f1a925c41d29d0949d5257ccc38aab [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from pathlib import Path
import click
from .core import Config, Repo, Queue, Target, Job, CrossbowError
from .reports import EmailReport, ConsoleReport
from ..utils.source import ArrowSources
_default_arrow_path = ArrowSources.find().path
_default_queue_path = _default_arrow_path.parent / "crossbow"
_default_config_path = _default_arrow_path / "dev" / "tasks" / "tasks.yml"
@click.group()
@click.option('--github-token', '-t', default=None,
envvar="CROSSBOW_GITHUB_TOKEN",
help='OAuth token for GitHub authentication')
@click.option('--arrow-path', '-a',
type=click.Path(), default=_default_arrow_path,
help='Arrow\'s repository path. Defaults to the repository of '
'this script')
@click.option('--queue-path', '-q',
type=click.Path(), default=_default_queue_path,
help='The repository path used for scheduling the tasks. '
'Defaults to crossbow directory placed next to arrow')
@click.option('--queue-remote', '-qr', default=None,
help='Force to use this remote URL for the Queue repository')
@click.option('--output-file', metavar='<output>',
type=click.File('w', encoding='utf8'), default='-',
help='Capture output result into file.')
@click.pass_context
def crossbow(ctx, github_token, arrow_path, queue_path, queue_remote,
output_file):
"""
Schedule packaging tasks or nightly builds on CI services.
"""
ctx.ensure_object(dict)
ctx.obj['output'] = output_file
ctx.obj['arrow'] = Repo(arrow_path)
ctx.obj['queue'] = Queue(queue_path, remote_url=queue_remote,
github_token=github_token, require_https=True)
@crossbow.command()
@click.option('--config-path', '-c',
type=click.Path(exists=True), default=_default_config_path,
help='Task configuration yml. Defaults to tasks.yml')
@click.pass_obj
def check_config(obj, config_path):
# load available tasks configuration and groups from yaml
config = Config.load_yaml(config_path)
config.validate()
output = obj['output']
config.show(output)
@crossbow.command()
@click.argument('tasks', nargs=-1, required=False)
@click.option('--group', '-g', 'groups', multiple=True,
help='Submit task groups as defined in task.yml')
@click.option('--param', '-p', 'params', multiple=True,
help='Additional task parameters for rendering the CI templates')
@click.option('--job-prefix', default='build',
help='Arbitrary prefix for branch names, e.g. nightly')
@click.option('--config-path', '-c',
type=click.Path(exists=True), default=_default_config_path,
help='Task configuration yml. Defaults to tasks.yml')
@click.option('--arrow-version', '-v', default=None,
help='Set target version explicitly.')
@click.option('--arrow-remote', '-r', default=None,
help='Set GitHub remote explicitly, which is going to be cloned '
'on the CI services. Note, that no validation happens '
'locally. Examples: https://github.com/apache/arrow or '
'https://github.com/kszucs/arrow.')
@click.option('--arrow-branch', '-b', default=None,
help='Give the branch name explicitly, e.g. master, ARROW-1949.')
@click.option('--arrow-sha', '-t', default=None,
help='Set commit SHA or Tag name explicitly, e.g. f67a515, '
'apache-arrow-0.11.1.')
@click.option('--fetch/--no-fetch', default=True,
help='Fetch references (branches and tags) from the remote')
@click.option('--dry-run/--commit', default=False,
help='Just display the rendered CI configurations without '
'committing them')
@click.option('--no-push/--push', default=False,
help='Don\'t push the changes')
@click.pass_obj
def submit(obj, tasks, groups, params, job_prefix, config_path, arrow_version,
arrow_remote, arrow_branch, arrow_sha, fetch, dry_run, no_push):
output = obj['output']
queue, arrow = obj['queue'], obj['arrow']
# load available tasks configuration and groups from yaml
config = Config.load_yaml(config_path)
try:
config.validate()
except CrossbowError as e:
raise click.ClickException(str(e))
# Override the detected repo url / remote, branch and sha - this aims to
# make release procedure a bit simpler.
# Note, that the target resivion's crossbow templates must be
# compatible with the locally checked out version of crossbow (which is
# in case of the release procedure), because the templates still
# contain some business logic (dependency installation, deployments)
# which will be reduced to a single command in the future.
target = Target.from_repo(arrow, remote=arrow_remote, branch=arrow_branch,
head=arrow_sha, version=arrow_version)
# parse additional job parameters
params = dict([p.split("=") for p in params])
# instantiate the job object
try:
job = Job.from_config(config=config, target=target, tasks=tasks,
groups=groups, params=params)
except CrossbowError as e:
raise click.ClickException(str(e))
job.show(output)
if dry_run:
return
if fetch:
queue.fetch()
queue.put(job, prefix=job_prefix)
if no_push:
click.echo('Branches and commits created but not pushed: `{}`'
.format(job.branch))
else:
queue.push()
click.echo('Pushed job identifier is: `{}`'.format(job.branch))
@crossbow.command()
@click.argument('task', required=True)
@click.option('--config-path', '-c',
type=click.Path(exists=True), default=_default_config_path,
help='Task configuration yml. Defaults to tasks.yml')
@click.option('--arrow-version', '-v', default=None,
help='Set target version explicitly.')
@click.option('--arrow-remote', '-r', default=None,
help='Set GitHub remote explicitly, which is going to be cloned '
'on the CI services. Note, that no validation happens '
'locally. Examples: https://github.com/apache/arrow or '
'https://github.com/kszucs/arrow.')
@click.option('--arrow-branch', '-b', default=None,
help='Give the branch name explicitly, e.g. master, ARROW-1949.')
@click.option('--arrow-sha', '-t', default=None,
help='Set commit SHA or Tag name explicitly, e.g. f67a515, '
'apache-arrow-0.11.1.')
@click.option('--param', '-p', 'params', multiple=True,
help='Additional task parameters for rendering the CI templates')
@click.pass_obj
def render(obj, task, config_path, arrow_version, arrow_remote, arrow_branch,
arrow_sha, params):
"""
Utility command to check the rendered CI templates.
"""
from .core import _flatten
def highlight(code):
try:
from pygments import highlight
from pygments.lexers import YamlLexer
from pygments.formatters import TerminalFormatter
return highlight(code, YamlLexer(), TerminalFormatter())
except ImportError:
return code
arrow = obj['arrow']
target = Target.from_repo(arrow, remote=arrow_remote, branch=arrow_branch,
head=arrow_sha, version=arrow_version)
config = Config.load_yaml(config_path)
params = dict([p.split("=") for p in params])
job = Job.from_config(config=config, target=target, tasks=[task],
params=params)
for task_name, rendered_files in job.render_tasks().items():
for path, content in _flatten(rendered_files).items():
click.echo('#' * 80)
click.echo('### {:^72} ###'.format("/".join(path)))
click.echo('#' * 80)
click.echo(highlight(content))
@crossbow.command()
@click.argument('job-name', required=True)
@click.option('--fetch/--no-fetch', default=True,
help='Fetch references (branches and tags) from the remote')
@click.pass_obj
def status(obj, job_name, fetch):
output = obj['output']
queue = obj['queue']
if fetch:
queue.fetch()
job = queue.get(job_name)
ConsoleReport(job).show(output)
@crossbow.command()
@click.argument('prefix', required=True)
@click.option('--fetch/--no-fetch', default=True,
help='Fetch references (branches and tags) from the remote')
@click.pass_obj
def latest_prefix(obj, prefix, fetch):
queue = obj['queue']
if fetch:
queue.fetch()
latest = queue.latest_for_prefix(prefix)
click.echo(latest.branch)
@crossbow.command()
@click.argument('job-name', required=True)
@click.option('--sender-name', '-n',
help='Name to use for report e-mail.')
@click.option('--sender-email', '-e',
help='E-mail to use for report e-mail.')
@click.option('--recipient-email', '-r',
help='Where to send the e-mail report')
@click.option('--smtp-user', '-u',
help='E-mail address to use for SMTP login')
@click.option('--smtp-password', '-P',
help='SMTP password to use for report e-mail.')
@click.option('--smtp-server', '-s', default='smtp.gmail.com',
help='SMTP server to use for report e-mail.')
@click.option('--smtp-port', '-p', default=465,
help='SMTP port to use for report e-mail.')
@click.option('--poll/--no-poll', default=False,
help='Wait for completion if there are tasks pending')
@click.option('--poll-max-minutes', default=180,
help='Maximum amount of time waiting for job completion')
@click.option('--poll-interval-minutes', default=10,
help='Number of minutes to wait to check job status again')
@click.option('--send/--dry-run', default=False,
help='Just display the report, don\'t send it')
@click.option('--fetch/--no-fetch', default=True,
help='Fetch references (branches and tags) from the remote')
@click.pass_obj
def report(obj, job_name, sender_name, sender_email, recipient_email,
smtp_user, smtp_password, smtp_server, smtp_port, poll,
poll_max_minutes, poll_interval_minutes, send, fetch):
"""
Send an e-mail report showing success/failure of tasks in a Crossbow run
"""
output = obj['output']
queue = obj['queue']
if fetch:
queue.fetch()
job = queue.get(job_name)
report = EmailReport(
job=job,
sender_name=sender_name,
sender_email=sender_email,
recipient_email=recipient_email
)
if poll:
job.wait_until_finished(
poll_max_minutes=poll_max_minutes,
poll_interval_minutes=poll_interval_minutes
)
if send:
report.send(
smtp_user=smtp_user,
smtp_password=smtp_password,
smtp_server=smtp_server,
smtp_port=smtp_port
)
else:
report.show(output)
@crossbow.command()
@click.argument('job-name', required=True)
@click.option('-t', '--target-dir',
default=_default_arrow_path / 'packages',
type=click.Path(file_okay=False, dir_okay=True),
help='Directory to download the build artifacts')
@click.option('--dry-run/--execute', default=False,
help='Just display process, don\'t download anything')
@click.option('--fetch/--no-fetch', default=True,
help='Fetch references (branches and tags) from the remote')
@click.pass_obj
def download_artifacts(obj, job_name, target_dir, dry_run, fetch):
"""Download build artifacts from GitHub releases"""
output = obj['output']
# fetch the queue repository
queue = obj['queue']
if fetch:
queue.fetch()
# query the job's artifacts
job = queue.get(job_name)
# create directory to download the assets to
target_dir = Path(target_dir).absolute() / job_name
target_dir.mkdir(parents=True, exist_ok=True)
# download the assets while showing the job status
def asset_callback(task_name, task, asset):
if asset is not None:
path = target_dir / task_name / asset.name
path.parent.mkdir(exist_ok=True)
if not dry_run:
asset.download(path)
click.echo('Downloading {}\'s artifacts.'.format(job_name))
click.echo('Destination directory is {}'.format(target_dir))
click.echo()
report = ConsoleReport(job)
report.show(output, asset_callback=asset_callback)
@crossbow.command()
@click.option('--sha', required=True, help='Target committish')
@click.option('--tag', required=True, help='Target tag')
@click.option('--method', default='curl', help='Use cURL to upload')
@click.option('--pattern', '-p', 'patterns', required=True, multiple=True,
help='File pattern to upload as assets')
@click.pass_obj
def upload_artifacts(obj, tag, sha, patterns, method):
queue = obj['queue']
queue.github_overwrite_release_assets(
tag_name=tag, target_commitish=sha, method=method, patterns=patterns
)