Skip to content

Adds Continuous Integration scripts to the repo. #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ captures/
local.properties
google-services.json
/build.gradle
_artifacts
25 changes: 25 additions & 0 deletions ci/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Continuous Integration

This directory contains tooling used to run Continuous Integration tasks.

## Prerequisites

- Requires python3.5+ and setuptools to be installed.

## Setup

- Optionally create a virtualenv to avoid installing system-wide.
```bash
python3 -m venv ~/.venvs/fireci
source ~/.venvs/fireci/bin/activate
```
- At the root of of the firebase sdk repo, run
```
./ci/fireci/setup.py develop
```

- For usage help, see:
```
fireci --help
```

5 changes: 5 additions & 0 deletions ci/fireci/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*.egg-info
.venv
__pycache__
bdist
dist
2 changes: 2 additions & 0 deletions ci/fireci/.style.yapf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[style]
based_on_style = chromium
13 changes: 13 additions & 0 deletions ci/fireci/fireci/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2018 Google LLC
#
# Licensed 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.
44 changes: 44 additions & 0 deletions ci/fireci/fireci/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright 2018 Google LLC
#
# Licensed 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.

import click
import os

from . import gradle
from .internal import ci_command


@click.argument('task', required=True, nargs=-1)
@click.option(
'--gradle-opts',
default='',
help='GRADLE_OPTS passed to the gradle invocation.')
@ci_command('gradle')
def gradle_command(task, gradle_opts):
"""Runs the specified gradle commands."""
gradle.run(*task, gradle_opts=gradle_opts)


@ci_command()
def smoke_tests_experimental():
"""Builds all SDKs in release mode and then tests test-apps against them."""
gradle.run('publishAllToBuildDir')

cwd = os.getcwd()
gradle.run(
'connectedReleaseAndroidTest',
gradle_opts='-Dmaven.repo.local={}'.format(
os.path.join(cwd, 'build', 'm2repository')),
workdir=os.path.join(cwd, 'test-apps'),
)
30 changes: 30 additions & 0 deletions ci/fireci/fireci/emulator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright 2018 Google LLC
#
# Licensed 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.

import logging

_logger = logging.getLogger('fireci.emulator')


# TODO(vkryachko): start/shutdown the emulator.
class EmulatorHandler:

def __init__(self, artifacts_dir):
self._artifacts_dir = artifacts_dir

def __enter__(self):
_logger.debug('Pretend to start the emulator(TODO)')

def __exit__(self, exception_type, exception_value, traceback):
_logger.debug('Pretend to stop the emulator(TODO)')
40 changes: 40 additions & 0 deletions ci/fireci/fireci/gradle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright 2018 Google LLC
#
# Licensed 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.

import logging
import os
import subprocess
import sys

_logger = logging.getLogger('fireci.gradle')

ADB_INSTALL_TIMEOUT = '5'


def run(*args, gradle_opts='', workdir=None):
"""Invokes gradle with specified args and gradle_opts."""
new_env = dict(os.environ)
if gradle_opts:
new_env['GRADLE_OPTS'] = gradle_opts
new_env[
'ADB_INSTALL_TIMEOUT'] = ADB_INSTALL_TIMEOUT # 5 minutes, rather than 2 minutes

command = ['./gradlew'] + list(args)
_logger.info('Executing gradle command: "%s" in directory: "%s"',
" ".join(command), workdir if workdir else '.')
return subprocess.check_call(
command,
cwd=workdir,
env=new_env,
)
128 changes: 128 additions & 0 deletions ci/fireci/fireci/internal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Copyright 2018 Google LLC
#
# Licensed 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.

import click
import contextlib
import functools
import glob
import itertools
import logging
import os
import shutil

from . import emulator

_logger = logging.getLogger('fireci')


def _ensure_dir(directory):
if not os.path.exists(directory):
os.makedirs(directory)


@contextlib.contextmanager
def _artifact_handler(target_directory, artifact_patterns):
_logger.debug(
'Artifacts will be searched for in directories matching {} patterns and placed in {}'
.format(artifact_patterns, target_directory))
try:
yield
finally:
_ensure_dir(target_directory)
paths = itertools.chain(*(glob.iglob(x, recursive=True)
for x in artifact_patterns))
for path in paths:
target_name = os.path.join(target_directory, "_".join(path.split('/')))
if os.path.isdir(path):
shutil.copytree(path, target_name)
else:
shutil.copyfile(path, target_name)


@contextlib.contextmanager
def _emulator_handler(enabled, target_directory):
if not enabled:
yield
return

with emulator.EmulatorHandler(target_directory):
yield


class _CommonOptions:
pass


_pass_options = click.make_pass_decorator(_CommonOptions, ensure=True)


@click.group()
@click.option(
'--artifact-target-dir',
default='_artifacts',
help='Directory where artifacts will be symlinked to.',
type=click.Path(dir_okay=True, resolve_path=True),
)
@click.option(
'--artifact-patterns',
default=('**/build/test-results', '**/build/reports'),
help=
'Shell-style artifact patterns that are symlinked into `artifact-target-dir`.'\
'Can be specified multiple times.',
multiple=True,
type=str,
)
@click.option(
'--with-emulator',
default=False,
help='Specifies whether to start an Android emulator a command executes.',
is_flag=True,
)
@_pass_options
def main(options, **kwargs):
"""Main command group.

Should be the "main" entrypoint of the binary.
"""
for k, v in kwargs.items():
setattr(options, k, v)


def ci_command(name=None):
"""Decorator to use for CI commands.

The differences from the standard @click.command are:

* Allows configuration of artifacts that are uploaded for later viewing in CI.
* Registers the command automatically

:param name: Optional name of the task. Defaults to the function name that is decorated with
this decorator.
"""

def ci_command(f):

@main.command(name=f.__name__ if name is None else name, help=f.__doc__)
@_pass_options
@click.pass_context
def new_func(ctx, options, *args, **kwargs):
with _artifact_handler(options.artifact_target_dir,
options.artifact_patterns), _emulator_handler(
options.with_emulator,
options.artifact_target_dir):
return ctx.invoke(f, *args, **kwargs)

return functools.update_wrapper(new_func, f)

return ci_command
25 changes: 25 additions & 0 deletions ci/fireci/fireci/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2018 Google LLC
#
# Licensed 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.

import logging

from . import commands
from .internal import main

logging.basicConfig(
format='%(name)s: [%(levelname)s] %(message)s',
level=logging.DEBUG,
)

cli = main
34 changes: 34 additions & 0 deletions ci/fireci/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
#
# Copyright 2018 Google LLC
#
# Licensed 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.

import os
from setuptools import find_packages, setup

os.chdir(os.path.abspath(os.path.dirname(__file__)))

requires = []

setup(
name='fireci',
version='0.1',
install_requires=[
'click==6.7',
],
packages=find_packages(exclude=['tests']),
entry_points={
'console_scripts': ['fireci = fireci.main:cli'],
},
)
17 changes: 17 additions & 0 deletions ci/fireci/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2018 Google LLC
#
# Licensed 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.

import logging

logging.disable(logging.CRITICAL)
Loading