├── deployer ├── __init__.py ├── exceptions.py ├── constants.py ├── cleaner.py ├── selfish.py ├── utils.py ├── checker.py ├── main.py ├── submodules.py ├── push.py └── localerefresh.py ├── .gitignore ├── setup.cfg ├── deploy.sh ├── .therapist.yml ├── setup.py ├── README.md └── LICENSE /deployer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | kuma_deployer.egg-info/ 4 | localerefresh.log 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | max-line-length = 88 6 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | pip install twine 5 | # From https://pypi.org/project/twine/ 6 | rm -fr dist/ 7 | python setup.py sdist bdist_wheel 8 | twine upload dist/* 9 | -------------------------------------------------------------------------------- /.therapist.yml: -------------------------------------------------------------------------------- 1 | actions: 2 | black: 3 | run: black --check --diff {files} 4 | fix: black {files} 5 | include: "*.py" 6 | 7 | flake8: 8 | run: flake8 {files} 9 | include: "*.py" 10 | -------------------------------------------------------------------------------- /deployer/exceptions.py: -------------------------------------------------------------------------------- 1 | class CoreException(Exception): 2 | """Exists for the benefit of making the cli easier to catch exceptions.""" 3 | 4 | 5 | class SubmoduleFindingError(CoreException): 6 | """when struggling to find the submodule.""" 7 | 8 | 9 | class DirtyRepoError(CoreException): 10 | """dirty repo, d'uh""" 11 | 12 | 13 | class MasterBranchError(CoreException): 14 | """Not on the right branch""" 15 | 16 | 17 | class PushBranchError(CoreException): 18 | """struggling to find the branch""" 19 | 20 | 21 | class RemoteURLError(CoreException): 22 | """when a remote's URL isn't awesome""" 23 | 24 | 25 | class PuenteVersionError(CoreException): 26 | """when something's wrong trying to figure out the next puente version""" 27 | -------------------------------------------------------------------------------- /deployer/constants.py: -------------------------------------------------------------------------------- 1 | import getpass 2 | import os 3 | 4 | from decouple import AutoConfig 5 | 6 | config = AutoConfig(os.curdir) 7 | 8 | 9 | def _current_user(): 10 | return getpass.getuser() 11 | 12 | 13 | GITHUB_ACCESS_TOKEN = config("GITHUB_ACCESS_TOKEN") 14 | KUMA_REPO_NAME = config("DEPLOYER_KUMA_REPO_NAME", "mozilla/kuma") # about to change! 15 | 16 | DEFAULT_MASTER_BRANCH = config("DEPLOYER_DEFAULT_MASTER_BRANCH", "master") 17 | DEFAULT_UPSTREAM_NAME = config("DEPLOYER_DEFAULT_UPSTREAM_NAME", "origin") 18 | DEFAULT_SUBMODULES_UPSTREAM_NAME = config( 19 | "DEPLOYER_DEFAULT_SUBMODULES_UPSTREAM_NAME", "origin" 20 | ) 21 | DEFAULT_YOUR_REMOTE_NAME = config("DEPLOYER_DEFAULT_YOUR_REMOTE_NAME", _current_user()) 22 | 23 | WHATSDEPLOYED_URL = config( 24 | "DEPLOYER_WHATSDEPLOYED_URL", "https://whatsdeployed.io/s/HC0/mozilla/kuma" 25 | ) 26 | STAGE_PUSH_BRANCH = config("DEPLOYER_STAGE_PUSH_BRANCH", "stage-push") 27 | STAGE_INTEGRATIONTEST_BRANCH = config( 28 | "DEPLOYER_STAGE_INTEGRATIONTEST_BRANCH", "stage-integration-tests" 29 | ) 30 | PROD_PUSH_BRANCH = config("DEPLOYER_PROD_PUSH_BRANCH", "prod-push") 31 | STANDBY_PUSH_BRANCH = config("DEPLOYER_STANDBY_PUSH_BRANCH", "standby-push") 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | 3 | from setuptools import find_packages, setup 4 | 5 | _here = path.dirname(__file__) 6 | 7 | 8 | dev_requirements = ["black==19.3b0", "flake8==3.7.8", "therapist"] 9 | 10 | setup( 11 | name="kuma-deployer", 12 | version="0.2.6", 13 | author="Peter Bengtsson", 14 | author_email="mail@peterbe.com", 15 | url="https://github.com/mdn/kuma-deployer", 16 | description="https://kuma.readthedocs.io/en/latest/deploy.html as a script", 17 | long_description=open(path.join(_here, "README.md")).read(), 18 | long_description_content_type="text/markdown", 19 | license="MPL 2.0", 20 | classifiers=[ 21 | "Programming Language :: Python", 22 | "Programming Language :: Python :: 3", 23 | "Programming Language :: Python :: Implementation :: CPython", 24 | "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", 25 | ], 26 | packages=find_packages(), 27 | include_package_data=True, 28 | zip_safe=False, 29 | install_requires=["GitPython", "click", "PyGithub", "python-decouple", "requests"], 30 | extras_require={"dev": dev_requirements}, 31 | entry_points=""" 32 | [console_scripts] 33 | kuma-deployer=deployer.main:cli 34 | """, 35 | setup_requires=[], 36 | tests_require=["pytest"], 37 | keywords="git github submodule", 38 | ) 39 | -------------------------------------------------------------------------------- /deployer/cleaner.py: -------------------------------------------------------------------------------- 1 | import git 2 | from github import Github, GithubException 3 | 4 | from .constants import GITHUB_ACCESS_TOKEN, KUMA_REPO_NAME 5 | from .exceptions import DirtyRepoError 6 | from .utils import info, success, warning 7 | 8 | 9 | def start_cleaner(repo_location, config): 10 | repo = git.Repo(repo_location) 11 | # Check if it's dirty 12 | if repo.is_dirty(): 13 | raise DirtyRepoError( 14 | 'The repo is currently "dirty". Stash or commit away.\n' 15 | f"Run `git status` inside {repo_location} to see what's up." 16 | ) 17 | 18 | # XXX I'd like to move away from branches and switch to tags. 19 | # But for now, just to check sanity do a-something. 20 | active_branch = repo.active_branch 21 | if active_branch.name.startswith("pre-push"): 22 | # Find its pull request and see if it's merged 23 | g = Github(GITHUB_ACCESS_TOKEN) 24 | g_repo = g.get_repo(KUMA_REPO_NAME) 25 | 26 | pulls = g_repo.get_pulls( 27 | sort="created", 28 | direction="desc", 29 | state="closed", 30 | head=active_branch.name, 31 | base="master", 32 | ) 33 | raise NotImplementedError("this doesn't work.") 34 | # Seems it always returns everything 35 | for pr in pulls: 36 | print(pr.number, pr.title, pr.head.ref, pr.base.ref, pr.merged) 37 | # print(dir(pr)) 38 | # break 39 | -------------------------------------------------------------------------------- /deployer/selfish.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | 4 | import git 5 | from github import Github 6 | 7 | from .constants import GITHUB_ACCESS_TOKEN, KUMA_REPO_NAME 8 | from .exceptions import DirtyRepoError 9 | from .utils import success, warning, info 10 | 11 | 12 | def self_check(repo_location, config): 13 | def pp_location(p): 14 | """return the location if displayed using the ~ notation 15 | for the user's home directory.""" 16 | home_dir = os.path.expanduser("~") 17 | if p.startswith(home_dir): 18 | return p.replace(home_dir, "~") 19 | return p 20 | 21 | info("Current config...") 22 | for k in sorted(config): 23 | info(f"{k:<30}: {config[k]}") 24 | print() # whitespace 25 | 26 | repo = git.Repo(repo_location) 27 | 28 | expect_remote_names = [config["upstream_name"], config["your_remote_name"]] 29 | for remote in repo.remotes: 30 | if remote.name in expect_remote_names: 31 | success(f"{pp_location(repo_location)} has a remote called {remote.name}") 32 | expect_remote_names.remove(remote.name) 33 | 34 | for name in expect_remote_names: 35 | warning(f"Warning! Couldn't find a remote named {name!r}") 36 | 37 | print() # whitespace 38 | 39 | if repo.is_dirty(): 40 | raise DirtyRepoError( 41 | 'The repo is currently "dirty". Stash or commit away.\n' 42 | f"Run `git status` inside {pp_location(repo_location)} to see what's up." 43 | ) 44 | success(f"Repo at {pp_location(repo_location)} is not dirty.") 45 | print() # whitespace 46 | 47 | for submodule in repo.submodules: 48 | submodule.update(init=True) 49 | sub_repo = submodule.module() 50 | if sub_repo.is_dirty(): 51 | raise DirtyRepoError(f"The git submodule {submodule!r} is dirty.") 52 | success(f"Git submodule {submodule.name!r} is not dirty.") 53 | 54 | # Check that it has remote named {submodules_upstream_name} 55 | for remote in sub_repo.remotes: 56 | if remote.name == config["submodules_upstream_name"]: 57 | break 58 | else: 59 | warning( 60 | f"The git submodule {submodule.name!r} does not have a remote " 61 | f"called {config['submodules_upstream_name']!r}!" 62 | ) 63 | 64 | g = Github(GITHUB_ACCESS_TOKEN) 65 | g_repo = g.get_repo(KUMA_REPO_NAME) 66 | pulls = g_repo.get_pulls( 67 | sort="created", direction="desc", state="open", base="master" 68 | ) 69 | for pr in pulls: 70 | if ( 71 | pr.title.startswith("Submodules: ") 72 | and re.findall(r"[a-f0-9]{7}\.\.\.[a-f0-9]{7}", pr.title) 73 | and pr.state != "closed" 74 | ): 75 | warning(f"Appears to have open pull request to update submodules! {pr.url}") 76 | break 77 | 78 | # XXX What else can we check? What about having access to Jenkins? 79 | -------------------------------------------------------------------------------- /deployer/utils.py: -------------------------------------------------------------------------------- 1 | import click 2 | import requests 3 | from requests.adapters import HTTPAdapter 4 | from requests.packages.urllib3.util.retry import Retry 5 | 6 | 7 | def error(*msg): 8 | msg = " ".join([str(x) for x in msg]) 9 | click.echo(click.style(msg, fg="red")) 10 | 11 | 12 | def warning(*msg): 13 | msg = " ".join([str(x) for x in msg]) 14 | click.echo(click.style(msg, fg="yellow")) 15 | 16 | 17 | def info(*msg): 18 | msg = " ".join([str(x) for x in msg]) 19 | click.echo(click.style(msg)) 20 | 21 | 22 | def success(*msg): 23 | msg = " ".join([str(x) for x in msg]) 24 | click.echo(click.style(msg, fg="green")) 25 | 26 | 27 | def requests_retry_session( 28 | retries=4, backoff_factor=0.4, status_forcelist=(500, 502, 504), session=None 29 | ): 30 | """Opinionated wrapper that creates a requests session with a 31 | HTTPAdapter that sets up a Retry policy that includes connection 32 | retries. 33 | 34 | If you do the more naive retry by simply setting a number. E.g.:: 35 | 36 | adapter = HTTPAdapter(max_retries=3) 37 | 38 | then it will raise immediately on any connection errors. 39 | Retrying on connection errors guards better on unpredictable networks. 40 | From http://docs.python-requests.org/en/master/api/?highlight=retries#requests.adapters.HTTPAdapter 41 | it says: "By default, Requests does not retry failed connections." 42 | 43 | The backoff_factor is documented here: 44 | https://urllib3.readthedocs.io/en/latest/reference/urllib3.util.html#urllib3.util.retry.Retry 45 | A default of retries=3 and backoff_factor=0.3 means it will sleep like:: 46 | 47 | [0.3, 0.6, 1.2] 48 | """ # noqa 49 | session = session or requests.Session() 50 | retry = Retry( 51 | total=retries, 52 | read=retries, 53 | connect=retries, 54 | backoff_factor=backoff_factor, 55 | status_forcelist=status_forcelist, 56 | ) 57 | adapter = HTTPAdapter(max_retries=retry) 58 | session.mount("http://", adapter) 59 | session.mount("https://", adapter) 60 | return session 61 | 62 | 63 | def _humanize_time(amount, units): 64 | """Chopped and changed from http://stackoverflow.com/a/6574789/205832""" 65 | intervals = (1, 60, 60 * 60, 60 * 60 * 24, 604800, 2419200, 29030400) 66 | names = ( 67 | ("second", "seconds"), 68 | ("minute", "minutes"), 69 | ("hour", "hours"), 70 | ("day", "days"), 71 | ("week", "weeks"), 72 | ("month", "months"), 73 | ("year", "years"), 74 | ) 75 | 76 | result = [] 77 | unit = [x[1] for x in names].index(units) 78 | # Convert to seconds 79 | amount = amount * intervals[unit] 80 | for i in range(len(names) - 1, -1, -1): 81 | a = int(amount) // intervals[i] 82 | if a > 0: 83 | result.append((a, names[i][1 % a])) 84 | amount -= a * intervals[i] 85 | return result 86 | 87 | 88 | def humanize_seconds(seconds): 89 | return "{} {}".format(*_humanize_time(seconds, "seconds")[0]) 90 | -------------------------------------------------------------------------------- /deployer/checker.py: -------------------------------------------------------------------------------- 1 | import git 2 | import requests 3 | 4 | from .utils import warning, info, success 5 | from .exceptions import MasterBranchError 6 | 7 | 8 | def check_builds(repo_location, config): 9 | repo = git.Repo(repo_location) 10 | # Are you on the "master" branch? 11 | active_branch = repo.active_branch 12 | if active_branch.name != config["master_branch"]: 13 | raise MasterBranchError( 14 | f"You first need to be on the {config['master_branch']!r} branch. " 15 | f"You're currently on the {active_branch.name!r} branch." 16 | ) 17 | 18 | # Check Kuma 19 | sha = repo.head.object.hexsha 20 | short_sha = repo.git.rev_parse(sha, short=7) 21 | info(f"Looking for Kuma short sha {short_sha}") 22 | public_url = "https://hub.docker.com/r/mdnwebdocs/kuma/tags" 23 | api_url = "https://registry.hub.docker.com/v2/repositories/mdnwebdocs/kuma/tags/" 24 | response = requests.get(api_url) 25 | response.raise_for_status() 26 | published_shas = [result["name"] for result in response.json()["results"]] 27 | if short_sha in published_shas: 28 | success(f"Kuma sha {short_sha} is on {public_url}") 29 | else: 30 | warning(f"Could not find {short_sha} on {public_url} :(") 31 | 32 | # Check Kumascript 33 | ks_repo = repo.submodules["kumascript"].module() 34 | sha = ks_repo.head.object.hexsha 35 | ks_short_sha = ks_repo.git.rev_parse(sha, short=7) 36 | info(f"Looking for Kumascript short sha {ks_short_sha}") 37 | public_url = "https://hub.docker.com/r/mdnwebdocs/kumascript/tags" 38 | api_url = ( 39 | "https://registry.hub.docker.com/v2/repositories/mdnwebdocs/kumascript/tags/" 40 | ) 41 | response = requests.get(api_url) 42 | response.raise_for_status() 43 | published_shas = [result["name"] for result in response.json()["results"]] 44 | if ks_short_sha in published_shas: 45 | success(f"Kumascript sha {ks_short_sha} is on {public_url}") 46 | else: 47 | warning(f"Could not find {ks_short_sha} on {public_url} :(") 48 | 49 | print("") 50 | 51 | # Check Kuma on Jenkins 52 | url = ( 53 | "https://ci.us-west-2.mdn.mozit.cloud" 54 | "/blue/organizations/jenkins/kuma/activity/?branch=master" 55 | ) 56 | info("Jenkins is both auth and VPN protected. Visit this URL and ...") 57 | info(f"Look for: {short_sha} on:") 58 | info(url) 59 | if input("Was it there? [Y/n] ").lower().strip() not in ("y", ""): 60 | warning("Hmm... Not sure what to think about that. Try in a couple of minutes?") 61 | return 62 | success("Great!\n") 63 | 64 | # Check Kuma on Jenkins 65 | url = ( 66 | "https://ci.us-west-2.mdn.mozit.cloud" 67 | "/blue/organizations/jenkins/kumascript/activity/?branch=master" 68 | ) 69 | info("Jenkins is both auth and VPN protected. Visit this URL and ...") 70 | info(f"Look for: {ks_short_sha} on:") 71 | info(url) 72 | if input("Was it there? [Y/n] ").lower().strip() not in ("y", ""): 73 | warning("Hmm... Not sure what to think about that. Try in a couple of minutes?") 74 | return 75 | success("Great!\nThere is hope in this world!") 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kuma-deployer 2 | 3 | In the ancient times of roaming saber tooth tigers and the smog over London 4 | hanging deep there was a time the poor serfs of Kuma had to trudge through 5 | [https://kuma.readthedocs.io/en/latest/deploy.html](https://kuma.readthedocs.io/en/latest/deploy.html) 6 | like some Kafkaesque slave of the man. That was then. This is now. 7 | 8 | Everything that can be automated in the Kuma deploy process is scripted here. 9 | 10 | ## Limitations and caveats 11 | 12 | **Hopefully, all temporary limitations and caveats.** 13 | 14 | There are some things that are hard to do such as pulling information out of Jenkins 15 | since it requires authentication and VPN. 16 | 17 | The other thing is that all the current commands are independent and users need to 18 | know which order to run them. Ideally, it should all be wrapped up into one single 19 | command but that's a little bit tricky since it requires waiting and external 20 | checking. 21 | 22 | ## Getting started 23 | 24 | You'll need a GitHub access token. 25 | Go to [github.com/settings/tokens](https://github.com/settings/tokens) and create a token, 26 | copy and paste it into your `.env` file or use `export`. E.g. 27 | 28 | cat .env 29 | GITHUB_ACCESS_TOKEN=a36f6736... 30 | 31 | pip install kuma-deployer 32 | kuma-deployer --help 33 | 34 | If you don't use a `.env` file you can use: 35 | 36 | GITHUB_ACCESS_TOKEN=a36f6736... kuma-deployer --help 37 | 38 | NOTE! The `.env` file (with the `GITHUB_ACCESS_TOKEN`) needs to be in the 39 | _current working directory_. I.e. where you are when you run `kuma-deployer`. So 40 | not necessarily where your `kuma` directory is (if these two are different). 41 | 42 | ## Goal 43 | 44 | The goal is that you simply install this script and type `kuma-deploy` and sit 45 | back and relax and with a bit of luck MDN is fully upgraded, deployment, and enabled. 46 | 47 | ## Contributing 48 | 49 | Clone this repo then run: 50 | 51 | pip install -e ".[dev]" 52 | 53 | That should have installed the CLI `kuma-deployer` 54 | 55 | kuma-deployer --help 56 | 57 | If you wanna make a PR, make sure it's formatted with `black` and passes `flake8`. 58 | 59 | You can check that all files are `flake8` fine by running: 60 | 61 | flake8 deployer 62 | 63 | And to check that all files are formatted according to `black` run: 64 | 65 | black --check deployer 66 | 67 | All of the code style stuff can be simplified by installing `therapist`. It should 68 | get installed by default, but setting it up as a `git` `pre-commit` hook is optional. 69 | Here's how you set it up once: 70 | 71 | therapist install 72 | 73 | Now, next time you try to commit a `.py` file with a `black` or `flake8` violation 74 | it will remind you and block the commit. You can override it like this: 75 | 76 | git commit -a -m "I know what I'm doing" 77 | 78 | To run _all_ code style and lint checkers you can also use `therapist` with: 79 | 80 | therapist run --use-tracked-files 81 | 82 | Some things can't be automatically fixed, but `black` violations can for example: 83 | 84 | therapist run --use-tracked-files --fix 85 | 86 | ## Contributing and using 87 | 88 | If you like to use the globally installed executable `kuma-deployer` but don't want 89 | to depend on a new PyPI release for every change you want to try, use this: 90 | 91 | # If you use a virtualenv, deactivate it first 92 | deactive 93 | # Use the global pip (or pip3) on your system 94 | pip3 install -e . 95 | 96 | If you do this, you can use this repo to install in your system. 97 | -------------------------------------------------------------------------------- /deployer/main.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import pkg_resources 3 | from pathlib import Path 4 | 5 | import click 6 | 7 | from .checker import check_builds 8 | from .cleaner import start_cleaner 9 | from .constants import ( 10 | DEFAULT_MASTER_BRANCH, 11 | DEFAULT_UPSTREAM_NAME, 12 | DEFAULT_SUBMODULES_UPSTREAM_NAME, 13 | DEFAULT_YOUR_REMOTE_NAME, 14 | ) 15 | from .exceptions import CoreException 16 | from .localerefresh import start_localerefresh 17 | from .push import prod_push, stage_push 18 | from .submodules import make_submodules_pr 19 | from .selfish import self_check 20 | from .utils import error, info 21 | 22 | 23 | def cli_wrap(fn): 24 | @functools.wraps(fn) 25 | def inner(*args, **kwargs): 26 | try: 27 | fn(*args, **kwargs) 28 | except CoreException as exception: 29 | info(exception.__class__.__name__) 30 | error(str(exception)) 31 | raise click.Abort 32 | 33 | return inner 34 | 35 | 36 | # XXX This all feels clunky! Useful functionality but annoying to have to repeat 37 | # the names so much. 38 | make_submodules_pr = cli_wrap(make_submodules_pr) 39 | start_cleaner = cli_wrap(start_cleaner) 40 | # start_localerefresh = cli_wrap(start_localerefresh) 41 | check_builds = cli_wrap(check_builds) 42 | stage_push = cli_wrap(stage_push) 43 | prod_push = cli_wrap(prod_push) 44 | self_check = cli_wrap(self_check) 45 | 46 | 47 | @click.group() 48 | @click.option( 49 | "--master-branch", 50 | default=DEFAULT_MASTER_BRANCH, 51 | help=f"name of main branch (default {DEFAULT_MASTER_BRANCH!r})", 52 | ) 53 | @click.option( 54 | "--upstream-name", 55 | default=DEFAULT_UPSTREAM_NAME, 56 | help=f"name of upstream remote (default {DEFAULT_UPSTREAM_NAME!r})", 57 | ) 58 | @click.option( 59 | "--submodules-upstream-name", 60 | default=DEFAULT_SUBMODULES_UPSTREAM_NAME, 61 | help=( 62 | f"name of upstream remote in submodules " 63 | f"(default {DEFAULT_SUBMODULES_UPSTREAM_NAME!r})" 64 | ), 65 | ) 66 | @click.option( 67 | "--your-remote-name", 68 | default=DEFAULT_YOUR_REMOTE_NAME, 69 | help=f"Name of your remote (default {DEFAULT_YOUR_REMOTE_NAME!r})", 70 | ) 71 | @click.option("--debug/--no-debug", default=False) 72 | @click.argument("kumarepo") 73 | @click.pass_context 74 | def cli( 75 | ctx, 76 | kumarepo, 77 | debug, 78 | master_branch, 79 | upstream_name, 80 | submodules_upstream_name, 81 | your_remote_name, 82 | ): 83 | ctx.ensure_object(dict) 84 | ctx.obj["kumarepo"] = kumarepo 85 | ctx.obj["debug"] = debug 86 | ctx.obj["master_branch"] = master_branch 87 | ctx.obj["upstream_name"] = upstream_name 88 | ctx.obj["submodules_upstream_name"] = submodules_upstream_name 89 | ctx.obj["your_remote_name"] = your_remote_name 90 | 91 | p = Path(kumarepo) 92 | if not p.exists(): 93 | error(f"{kumarepo} does not exist") 94 | raise click.Abort 95 | if not (p / ".git").exists(): 96 | error(f"{p / '.git'} does not exist so it's not a git repo") 97 | raise click.Abort 98 | 99 | 100 | @cli.command() 101 | @click.pass_context 102 | def clean(ctx): 103 | start_cleaner(ctx.obj["kumarepo"], ctx.obj) 104 | 105 | 106 | @cli.command() 107 | @click.pass_context 108 | def submodules(ctx): 109 | make_submodules_pr(ctx.obj["kumarepo"], ctx.obj) 110 | 111 | 112 | @cli.command() 113 | @click.pass_context 114 | @cli_wrap 115 | def l10n(ctx): 116 | start_localerefresh(ctx.obj["kumarepo"], ctx.obj) 117 | 118 | 119 | @cli.command() 120 | @click.pass_context 121 | def checkbuilds(ctx): 122 | check_builds(ctx.obj["kumarepo"], ctx.obj) 123 | 124 | 125 | @cli.command() 126 | @click.pass_context 127 | def stagepush(ctx): 128 | stage_push(ctx.obj["kumarepo"], ctx.obj) 129 | 130 | 131 | @cli.command() 132 | @click.pass_context 133 | def prodpush(ctx): 134 | prod_push(ctx.obj["kumarepo"], ctx.obj) 135 | 136 | 137 | @cli.command() 138 | @click.pass_context 139 | def selfcheck(ctx): 140 | self_check(ctx.obj["kumarepo"], ctx.obj) 141 | 142 | 143 | @cli.command() 144 | @click.pass_context 145 | def version(ctx): 146 | info(pkg_resources.get_distribution("kuma-deployer").version) 147 | -------------------------------------------------------------------------------- /deployer/submodules.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import git 4 | from github import Github, GithubException 5 | 6 | from .constants import GITHUB_ACCESS_TOKEN, KUMA_REPO_NAME 7 | from .exceptions import DirtyRepoError, MasterBranchError, SubmoduleFindingError 8 | from .utils import info, success, warning 9 | 10 | 11 | def make_submodules_pr( 12 | repo_location, config, accept_dirty=False, branch_name=None, only_submodules=None 13 | ): 14 | repo = git.Repo(repo_location) 15 | # Check if it's dirty 16 | if repo.is_dirty() and not accept_dirty: 17 | raise DirtyRepoError( 18 | 'The repo is currently "dirty". Stash or commit away.\n' 19 | f"Run `git status` inside {repo_location} to see what's up." 20 | ) 21 | 22 | # Are you on the "master" branch? 23 | active_branch = repo.active_branch 24 | if active_branch.name != config[ 25 | "master_branch" 26 | ] and not active_branch.name.startswith("pre-push"): 27 | raise MasterBranchError( 28 | f"You first need to be on the {config['master_branch']!r} branch. " 29 | f"You're currently on the {active_branch.name!r} branch." 30 | ) 31 | 32 | now = datetime.datetime.utcnow() 33 | branch_name = branch_name or f"pre-push-{now.strftime('%Y-%m-%d')}" 34 | 35 | # Come up with a pre-push branch name 36 | # existing_branch_names = [x.name for x in repo.heads] 37 | for head in repo.heads: 38 | if head.name == branch_name: 39 | print(f"Branch {head.name!r} already exists. Wanna re-use?") 40 | if input(f"Check out {head.name!r}? [y/N] ").lower() == "y": 41 | head.checkout() 42 | break 43 | else: 44 | raise NotImplementedError("Branch already exists and you don't want it") 45 | else: 46 | # Create the new branch 47 | new_branch = repo.create_head(branch_name) 48 | new_branch.checkout() 49 | 50 | # Check out all the latest and greatest submodules 51 | actual_updates = {} 52 | upstream_name = config.get("submodules_upstream_name") or config["upstream_name"] 53 | for submodule in repo.submodules: 54 | if only_submodules and submodule.name not in only_submodules: 55 | continue 56 | 57 | sub_repo = submodule.module() 58 | submodule.update(init=True) 59 | sub_repo.git.checkout(config["master_branch"]) 60 | sha = sub_repo.head.object.hexsha 61 | short_sha = sub_repo.git.rev_parse(sha, short=7) 62 | for remote in sub_repo.remotes: 63 | if remote.name == upstream_name: 64 | break 65 | else: 66 | raise SubmoduleFindingError(f"Can't find origin {upstream_name!r}") 67 | remote.pull(config["master_branch"]) 68 | 69 | sha2 = sub_repo.head.object.hexsha 70 | short_sha2 = sub_repo.git.rev_parse(sha2, short=7) 71 | 72 | if sha != sha2: 73 | info(f"Submodule {submodule.name!r} from {short_sha} to {short_sha2}") 74 | actual_updates[submodule.name] = [short_sha, short_sha2] 75 | else: 76 | warning(f"Submodule {submodule.name!r} already latest and greatest.") 77 | 78 | # actual_updates = {"kumascript": ["b70bab1", "9c29f10"]} 79 | 80 | if actual_updates: 81 | msg = f"Submodule{'s' if len(actual_updates) > 1 else ''}:" 82 | for name in sorted(actual_updates): 83 | if not msg.endswith(":"): 84 | msg += ", " 85 | shas = actual_updates[name] 86 | msg += f" {name} {shas[0]}...{shas[1]}" 87 | msg = msg.strip() 88 | info("About to commit with this message:", msg) 89 | repo.git.add(A=True) 90 | repo.git.commit(["-m", msg, "--no-verify"]) 91 | pushed = repo.git.push(config["your_remote_name"], branch_name) 92 | print("PUSHED:", repr(pushed)) 93 | 94 | head_name = f"{config['your_remote_name']}:{branch_name}" 95 | try: 96 | g = Github(GITHUB_ACCESS_TOKEN) 97 | g_repo = g.get_repo(KUMA_REPO_NAME) 98 | 99 | # Would be cool if this could list the difference! 100 | body = ( 101 | f"Updating the submodule{'s' if len(actual_updates) > 1 else ''}! 😊\n" 102 | ) 103 | 104 | created_pr = g_repo.create_pull(msg, body, "master", head_name) 105 | success(f"Now go and patiently wait for {created_pr.html_url} to go green.") 106 | 107 | except GithubException as exception: 108 | warning("GitHub integration failed:", exception) 109 | 110 | info("You can manually create the pull request:") 111 | create_pr_url = ( 112 | f"https://github.com/{KUMA_REPO_NAME}/compare/master..." 113 | f"{config['your_remote_name']}:{branch_name}?expand=1" 114 | ) 115 | success(f"\n\t{create_pr_url}\n") 116 | 117 | # Back to master branch 118 | repo.heads[config["master_branch"]].checkout() 119 | 120 | print("\n") 121 | info( 122 | f"After the PR has been merged, the branch {branch_name!r} can be removed:" 123 | ) 124 | success(f"\n\tgit branch -d {branch_name}") 125 | info("\n\t# optional, if you didn't already delete the remote branch...") 126 | success(f"\n\tgit push {config['your_remote_name']} :{branch_name}\n") 127 | -------------------------------------------------------------------------------- /deployer/push.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import time 3 | from urllib.parse import urlparse 4 | 5 | import click 6 | import git 7 | 8 | from .constants import ( 9 | PROD_PUSH_BRANCH, 10 | STAGE_INTEGRATIONTEST_BRANCH, 11 | STAGE_PUSH_BRANCH, 12 | STANDBY_PUSH_BRANCH, 13 | ) 14 | from .exceptions import ( 15 | DirtyRepoError, 16 | PushBranchError, 17 | RemoteURLError, 18 | MasterBranchError, 19 | ) 20 | from .utils import info, success, warning, requests_retry_session 21 | 22 | 23 | def center(msg): 24 | t_width, _ = shutil.get_terminal_size(fallback=(80, 24)) 25 | warning(f"----- {msg} ".ljust(t_width, "-")) 26 | 27 | 28 | def stage_push(repo_location, config): 29 | center(f"STAGE {STAGE_PUSH_BRANCH!r}") 30 | push(repo_location, config, STAGE_PUSH_BRANCH) 31 | 32 | print("\n") # some deliberate whitespace 33 | center(f"STAGE {STAGE_INTEGRATIONTEST_BRANCH!r}") 34 | push(repo_location, config, STAGE_INTEGRATIONTEST_BRANCH, show_whatsdeployed=False) 35 | info( 36 | "\nNow sit back and hold out for some sweat success of the integration tests. " 37 | "Check out #mdn-infra, on Slack, to for the joyous announcements. \n" 38 | "https://mozilla.slack.com/messages/CAVQJ18E6" 39 | ) 40 | info( 41 | "\nYou can also check out the results by visiting:\n\t" 42 | "https://ci.us-west-2.mdn.mozit.cloud/blue/organizations/jenkins/kuma/branches/" 43 | ) 44 | 45 | print("\n") # deliberate whitespace 46 | start_watching_for_change("https://developer.allizom.org/media/revision.txt") 47 | 48 | 49 | def prod_push(repo_location, config): 50 | center(f"PROD {PROD_PUSH_BRANCH!r}") 51 | push(repo_location, config, PROD_PUSH_BRANCH) 52 | 53 | info( 54 | "\nAfter Whatsdeploy says it's up, go troll and lurk on:\n\t" 55 | "https://rpm.newrelic.com/accounts/1807330/applications/" 56 | "72968468/traced_errors\n" 57 | ) 58 | 59 | print("\n") # some deliberate whitespace 60 | center(f"PROD {STANDBY_PUSH_BRANCH!r}") 61 | push(repo_location, config, STANDBY_PUSH_BRANCH) 62 | 63 | print("\n") # deliberate whitespace 64 | start_watching_for_change("https://developer.mozilla.org/media/revision.txt") 65 | 66 | 67 | def push(repo_location, config, branch, show_whatsdeployed=True, show_jenkins=True): 68 | repo = git.Repo(repo_location) 69 | # Check if it's dirty 70 | if repo.is_dirty(): 71 | raise DirtyRepoError( 72 | 'The repo is currently "dirty". Stash or commit away.\n' 73 | f"Run `git status` inside {repo_location} to see what's up." 74 | ) 75 | 76 | # Are you on the "master" branch? 77 | active_branch = repo.active_branch 78 | if active_branch.name == config["master_branch"]: 79 | # Need to check that it's up to date. 80 | # But before that can be done we need to git fetch origin. 81 | upstream_remote = repo.remotes[config["upstream_name"]] 82 | info(f"Fetching all branches from {config['upstream_name']}") 83 | upstream_remote.fetch() 84 | remote_master_branch = f"{config['upstream_name']}/{config['master_branch']}" 85 | diff = repo.git.diff(remote_master_branch) 86 | if diff: 87 | warning( 88 | f"Your local {config['master_branch']} is different from " 89 | f"{remote_master_branch!r}." 90 | ) 91 | if click.confirm( 92 | f"Want to pull latest {remote_master_branch!r}", default=True 93 | ): 94 | upstream_remote.pull(config["master_branch"]) 95 | info( 96 | f"Pulled latest {config['master_branch']} from " 97 | f"{config['upstream_name']}." 98 | ) 99 | else: 100 | warning("Godspeed!") 101 | else: 102 | msg = ( 103 | f"You're not on the {config['master_branch']!r} branch. " 104 | f"You're on {active_branch.name!r}." 105 | ) 106 | warning(msg) 107 | if not click.confirm("Are you sure you want to proceed?"): 108 | raise MasterBranchError( 109 | f"Bailing because not in {config['master_branch']!r}" 110 | ) 111 | 112 | # Kuma 113 | short_sha = _push_repo(repo, config, branch) 114 | success( 115 | f"Kuma: " f"Latest {branch!r} branch pushed to {config['upstream_name']!r}\n" 116 | ) 117 | if show_jenkins: 118 | sha = repo.head.object.hexsha 119 | short_sha = repo.git.rev_parse(sha, short=7) 120 | jenkins_url = ( 121 | f"https://ci.us-west-2.mdn.mozit.cloud/blue/organizations/jenkins/" 122 | f"kuma/activity?branch={branch}" 123 | ) 124 | info(f"Now, look for {short_sha} in\n\t{jenkins_url}") 125 | if show_whatsdeployed: 126 | info("Keep an eye on\n\thttps://whatsdeployed.io/s/HC0/mozilla/kuma") 127 | 128 | print("\n") # Some whitespace before Kumascript 129 | 130 | # Kumascript 131 | ks_repo = repo.submodules["kumascript"].module() 132 | # just in case it was detached 133 | ks_repo.heads[config["master_branch"]].checkout() 134 | ks_remote = ks_repo.remotes[config["upstream_name"]] 135 | for url in ks_remote.urls: 136 | if "git://" in url: 137 | parsed = urlparse(url) 138 | better_url = f"git@{parsed.netloc}:{parsed.path[1:]}" 139 | raise RemoteURLError( 140 | f"Won't be able to push to {url}. Run something like:\n" 141 | f"\n\tcd kumascript\n" 142 | f"\tgit remote set-url {config['upstream_name']} {better_url}\n" 143 | ) 144 | short_sha = _push_repo(ks_repo, config, branch) 145 | success( 146 | f"Kumascript: " 147 | f"Latest {branch!r} branch pushed to {config['upstream_name']!r}" 148 | ) 149 | if show_jenkins: 150 | jenkins_url = ( 151 | f"https://ci.us-west-2.mdn.mozit.cloud/blue/organizations/jenkins/" 152 | f"kumascript/activity?branch={branch}" 153 | ) 154 | info(f"Now, look for {short_sha} in\n\t{jenkins_url}") 155 | if show_whatsdeployed: 156 | info("Keep an eye on\n\thttps://whatsdeployed.io/s/SWJ/mdn/kumascript") 157 | 158 | 159 | def _push_repo(repo, config, branch_name): 160 | upstream_remote = repo.remotes[config["upstream_name"]] 161 | upstream_remote.fetch() 162 | 163 | active_branch = repo.active_branch 164 | if active_branch.name != branch_name: 165 | if branch_name not in [head.name for head in repo.heads]: 166 | stage_branch = repo.create_head(branch_name) 167 | 168 | else: 169 | for head in repo.heads: 170 | if head.name == branch_name: 171 | stage_branch = head 172 | break 173 | else: 174 | raise PushBranchError(f"Can't check out branch {branch_name!r}") 175 | 176 | stage_branch.checkout() 177 | 178 | # Merge the origin master branch into this 179 | origin_master_branch = f"{config['upstream_name']}/{config['master_branch']}" 180 | repo.git.merge(origin_master_branch) 181 | sha = repo.head.object.hexsha 182 | short_sha = repo.git.rev_parse(sha, short=7) 183 | repo.git.push(config["upstream_name"], branch_name) 184 | 185 | # Back to master branch 186 | repo.heads[config["master_branch"]].checkout() 187 | 188 | return short_sha 189 | 190 | 191 | def start_watching_for_change(url, sleep_seconds=10): 192 | session = requests_retry_session() 193 | response = session.get(url) 194 | response.raise_for_status() 195 | 196 | def fmt_seconds(delta): 197 | seconds = int(delta) 198 | if seconds > 60: 199 | minutes = seconds // 60 200 | seconds = seconds % 60 201 | return ( 202 | f"{minutes} minute{'s' if minutes > 1 else ''} " 203 | f"{seconds} second{'s' if seconds > 1 else ''} " 204 | ) 205 | return f"{seconds} second{'s' if seconds > 1 else ''} " 206 | 207 | t0 = time.time() 208 | first_content = response.text 209 | info(f"Watching for changes to output from {url}") 210 | print(f"(checking every {sleep_seconds} seconds)") 211 | while True: 212 | time.sleep(10) 213 | 214 | response = session.get(url) 215 | response.raise_for_status() 216 | new_content = response.text 217 | if new_content != first_content: 218 | success(f"Output from {url} has changed!\n") 219 | info(f"from {first_content!r} to {new_content!r}") 220 | info("Stopping the watcher. Bye!") 221 | break 222 | else: 223 | print(f"Been checking for {fmt_seconds(time.time() - t0)}") 224 | -------------------------------------------------------------------------------- /deployer/localerefresh.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import shlex 4 | import subprocess 5 | import datetime 6 | import sys 7 | import time 8 | from pathlib import Path 9 | 10 | import click 11 | import git 12 | 13 | from .submodules import make_submodules_pr 14 | from .utils import info, success, warning, humanize_seconds 15 | from .exceptions import ( 16 | PuenteVersionError, 17 | DirtyRepoError, 18 | MasterBranchError, 19 | SubmoduleFindingError, 20 | ) 21 | 22 | 23 | def run_command(command): 24 | process = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) 25 | while True: 26 | output = process.stdout.readline() 27 | if output == "" and process.poll() is not None: 28 | break 29 | if output: 30 | print(output.strip()) 31 | rc = process.poll() 32 | return rc 33 | 34 | 35 | puente_version_regex = re.compile(r'([\'"]VERSION[\'"]:\s*[\'"]([\d\.]+)[\'"])') 36 | 37 | 38 | def start_localerefresh(repo_location, config): 39 | repo = git.Repo(repo_location) 40 | 41 | if 0: 42 | raise NotImplementedError( 43 | "Need to check that your remote URL is: " 44 | "git remote set-url origin git@github.com:mozilla-l10n/mdn-l10n.git" 45 | ) 46 | 47 | # Check if it's dirty 48 | if repo.is_dirty(): 49 | raise DirtyRepoError( 50 | 'The repo is currently "dirty". Stash or commit away.\n' 51 | f"Run `git status` inside {repo_location} to see what's up.\n" 52 | "Or, perhaps run 'git submodule update'" 53 | ) 54 | 55 | # Are you on the "master" branch? 56 | active_branch = repo.active_branch 57 | if active_branch.name != config["master_branch"]: 58 | raise MasterBranchError( 59 | f"You first need to be on the {config['master_branch']!r} branch. " 60 | f"You're currently on the {active_branch.name!r} branch." 61 | ) 62 | 63 | settings_file = Path(repo_location) / "kuma" / "settings" / "common.py" 64 | next_puente_version = None 65 | with open(settings_file) as f: 66 | kuma_settings_content = f.read() 67 | for __, found in puente_version_regex.findall(kuma_settings_content): 68 | year, increment = [int(x) for x in found.split(".")] 69 | current_year = datetime.datetime.utcnow().year 70 | if year != current_year: 71 | # First one of the new year! 72 | next_puente_version = f"{current_year}.01" 73 | else: 74 | next_puente_version = f"{year}.{increment + 1:<2}" 75 | info( 76 | f"Current PUENTE version: {found} - " 77 | f"Next PUENTE version: {next_puente_version}" 78 | ) 79 | 80 | if not next_puente_version: 81 | raise PuenteVersionError("Can't find existing PUENTE version") 82 | 83 | locale_repo = repo.submodules["locale"].module() 84 | # Check out the master branch inside the 'locale' submodule 85 | # locale_repo.heads[config["master_branch"]].checkout() 86 | locale_repo.heads["master"].checkout() 87 | for remote in locale_repo.remotes: 88 | if remote.name == "origin": 89 | break 90 | else: 91 | raise SubmoduleFindingError(f"Can't find origin 'origin'") 92 | remote.pull(config["master_branch"]) 93 | 94 | cmd = "docker-compose exec web make localerefresh" 95 | 96 | filename = "localerefresh.log" 97 | info(f"Hold my 🍺 whilst I run {cmd!r} (logging in {filename} too)") 98 | t0 = time.time() 99 | 100 | with open(filename, "wb") as f: # replace 'w' with 'wb' for Python 3 101 | process = subprocess.Popen( 102 | shlex.split(cmd), stdout=subprocess.PIPE, cwd=repo_location 103 | ) 104 | for line in iter(process.stdout.readline, b""): 105 | sys.stdout.write(line.decode("utf-8")) 106 | f.write(line) 107 | 108 | t1 = time.time() 109 | 110 | seconds = int(t1 - t0) 111 | success( 112 | f"Sucessfully ran localerefresh. " 113 | f"Only took {seconds % 3600 // 60} minutes, {seconds % 60} seconds." 114 | ) 115 | 116 | new_msgids = set() 117 | total_diff_files = 0 118 | total_diff_lines = 0 119 | for diff in locale_repo.index.diff(None, create_patch=True): 120 | total_diff_files += 1 121 | for line in diff.diff.splitlines(): 122 | total_diff_lines += 1 123 | if line.startswith(b"+msgid") and line != b'+msgid ""': 124 | new_msgids.add(line) 125 | 126 | info(f"{total_diff_files:,} files and {total_diff_lines:,} lines in the diff") 127 | 128 | if new_msgids: 129 | info(f"There are {len(new_msgids)} new '+msgid' strings.") 130 | if click.confirm("Wanna see a list of these new additions?", default=True): 131 | for line in new_msgids: 132 | info(f"\t{line.decode('utf-8')}") 133 | print("") # Some whitespace 134 | if not click.confirm( 135 | "Do you want to proceed and commit this diff?", default=bool(new_msgids) 136 | ): 137 | info("Fine! Be like that!") 138 | info("To reset all changes to the submodules run:") 139 | info("\tgit submodule foreach git reset --hard") 140 | return 141 | 142 | # Gather all untracked files 143 | all_untracked_files = {} 144 | for path in locale_repo.untracked_files: 145 | path = os.path.join(locale_repo.working_dir, path) 146 | root = path.split(os.path.sep)[0] 147 | if root not in all_untracked_files: 148 | all_untracked_files[root] = { 149 | "files": [], 150 | "total_count": count_files_in_directory(root), 151 | } 152 | all_untracked_files[root]["files"].append(path) 153 | # Now filter this based on it being single files or a bunch 154 | untracked_files = {} 155 | for root, data in all_untracked_files.items(): 156 | for path in data["files"]: 157 | age = time.time() - os.stat(path).st_mtime 158 | # If there's fewer untracked files in its directory, suggest 159 | # the directory instead. 160 | if data["total_count"] == 1: 161 | path = root 162 | if path in untracked_files: 163 | if age < untracked_files[path]: 164 | # youngest file in that directory 165 | untracked_files[path] = age 166 | else: 167 | untracked_files[path] = age 168 | 169 | if untracked_files: 170 | ordered = sorted(untracked_files.items(), key=lambda x: x[1], reverse=True) 171 | warning("There are untracked files in the locale submodule") 172 | for path, age in ordered: 173 | if os.path.isdir(path): 174 | path = path + "/" 175 | print("\t", path.ljust(60), humanize_seconds(age), "old") 176 | 177 | if not click.confirm("Wanna ignore those?", default=True): 178 | info("Go ahead and address those untracked files.") 179 | return 180 | 181 | # Now we're going to do the equivalent of `git commit -a -m "..."` 182 | index = locale_repo.index 183 | files_added = [] 184 | files_removed = [] 185 | for x in locale_repo.index.diff(None): 186 | if x.deleted_file: 187 | files_removed.append(x.b_path) 188 | else: 189 | files_added.append(x.b_path) 190 | files_new = [] 191 | for x in locale_repo.index.diff(locale_repo.head.commit): 192 | files_new.append(x.b_path) 193 | if files_added: 194 | index.add(files_added) 195 | print("ADD", files_added) 196 | if files_removed: 197 | index.remove(files_removed) 198 | 199 | msg = f"Update strings {next_puente_version}" 200 | msg += "\n\n" 201 | msg += "New strings are as follows:\n" 202 | for line in new_msgids: 203 | msg += f"\t{line.decode('utf-8')}\n" 204 | # Do it like this (instead of `repo.git.commit(msg)`) 205 | # so that git signing works. 206 | locale_repo.git.commit(["--no-verify", "-m", msg]) 207 | success("Committed 'locale' changes with the following commit message:") 208 | info(msg) 209 | 210 | # Now, push to the origin 211 | locale_repo.git.push("origin", "master") 212 | 213 | def next_puente_version_replacer(match): 214 | return match.group().replace(match.groups()[1], next_puente_version) 215 | 216 | new_kuma_settings_content = puente_version_regex.sub( 217 | next_puente_version_replacer, kuma_settings_content 218 | ) 219 | assert new_kuma_settings_content != kuma_settings_content 220 | with open(settings_file, "w") as f: 221 | f.write(new_kuma_settings_content) 222 | success(f"Editing {settings_file} for {next_puente_version!r}") 223 | 224 | sha = locale_repo.head.object.hexsha 225 | short_sha = locale_repo.git.rev_parse(sha, short=7) 226 | 227 | make_submodules_pr( 228 | repo_location, 229 | config, 230 | accept_dirty=True, 231 | branch_name=f"locale-update-{next_puente_version}-{short_sha}", 232 | only_submodules=("locale",), 233 | ) 234 | 235 | 236 | def count_files_in_directory(directory): 237 | count = 0 238 | for root, _, files in os.walk(directory): 239 | # We COULD crosscheck these files against the .gitignore 240 | # if we ever felt overachievious. 241 | count += len(files) 242 | return count 243 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------