From 80ef535b1932ccfd2d96c43b1992cded1fe06da1 Mon Sep 17 00:00:00 2001 From: ebreton Date: Mon, 28 May 2018 19:44:06 +0200 Subject: [PATCH] initial revision --- Makefile | 198 +++++++++++++++++++++++++++++++++++++++ Pipfile | 12 +++ update_release.py | 232 ++++++++++++++++++++++++++++++++++++++++++++++ versions.py | 16 ++++ 4 files changed, 458 insertions(+) create mode 100644 Makefile create mode 100644 Pipfile create mode 100755 update_release.py create mode 100644 versions.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0540403 --- /dev/null +++ b/Makefile @@ -0,0 +1,198 @@ +#!make +# Default values, can be overridden either on the command line of make +# or in .env + +.PHONY: dev qa prod vars \ + cli-version ps shell logs stop pull restart restart-prod upgrade upgrade-prod \ + app-version release push-qa push-prod update-changelog + +VERSION:=$(shell python update_release.py -v) + +dev: check-env + # Simply start a ghost container making it directly available through $$PORT + docker run --rm -d --name ${NAME} \ + -v $(shell pwd)/instances/${NAME}:/var/lib/ghost/content \ + -p ${PORT}:2368 \ + -e url=http://${DOMAIN}:${PORT} \ + ghost:1-alpine + +qa: check-env + # Start a ghost container behind traefik (therefore available through 80 or 443), on path $$NAME + # Beware of --network used, which is the same one traefik should be using + docker run --rm -d --name ${NAME} \ + -v $(shell pwd)/instances/${NAME}:/var/lib/ghost/content \ + -e url=${PROTOCOL}://${DOMAIN}/${URI} \ + --network=proxy \ + --label "traefik.enable=true" \ + --label "traefik.backend=${NAME}" \ + --label "traefik.frontend.entryPoints=${PROTOCOL}" \ + --label "traefik.frontend.rule=Host:${DOMAIN};PathPrefix:/${URI}" \ + ghost:1-alpine + +# for backward compatibility +traefik: qa + @echo "" + @echo "!! DEPRECATION WARNING: 'make traefik' is replaced by 'make qa'. This command will be dropped in version 0.4" + +prod: check-prod-env + # Same configuration as make `traefik`, specifying DB + docker run --rm -d --user node --name ${NAME} \ + -v $(shell pwd)/instances/${NAME}:/var/lib/ghost/content \ + -e database__client=mysql \ + -e database__connection__host=db-shared \ + -e database__connection__user=root \ + -e database__connection__password=${MYSQL_ROOT_PASSWORD} \ + -e database__connection__database=${NAME} \ + -e mail__transport=SMTP \ + -e mail__options__service=Mailgun \ + -e mail__options__auth__user=${MAILGUN_LOGIN} \ + -e mail__options__auth__pass=${MAILGUN_PASSWORD} \ + -e url=${PROTOCOL}://${DOMAIN}/${URI} \ + --network=proxy \ + --label "traefik.enable=true" \ + --label "traefik.backend=${NAME}" \ + --label "traefik.frontend.entryPoints=${PROTOCOL}" \ + --label "traefik.frontend.rule=Host:${DOMAIN};PathPrefix:/${URI}" \ + ghost:1-alpine + +vars: check-env + # values used for dev + @echo ' PORT=${PORT}' + # values used by traefik (qa & prod) + @echo ' NAME=${NAME}' + @echo ' PROTOCOL=${PROTOCOL}' + @echo ' DOMAIN=${DOMAIN}' + @echo ' URI=${URI}' + # values used for prod + @echo ' MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}' + @echo ' MAILGUN_LOGIN=${MAILGUN_LOGIN}' + @echo ' MAILGUN_PASSWORD=${MAILGUN_PASSWORD}' + # values used for load tests + @echo ' GATLING_BASE_URL=${GATLING_BASE_URL}' + @echo ' GATLING_USERS=${GATLING_USERS}' + @echo ' GATLING_RAMP=${GATLING_RAMP}' + +check-prod-env: +ifeq ($(wildcard etc/prod.env),) + @echo "etc/prod.env file is missing" + @exit 1 +else +include etc/prod.env +export +endif + +check-env: +ifeq ($(wildcard .env),) + @echo ".env file is missing" + @exit 1 +else +include .env +export +endif + +GATLING_BASE_URL?=${PROTOCOL}://${NAME}:2368/${URI} +GATLING_USERS?=10 +GATLING_RAMP?=20 + +gatling: + docker run -it --rm \ + -v $(shell pwd)/etc/gatling-conf.scala:/opt/gatling/user-files/simulations/ghost/GhostFrontend.scala \ + -v $(shell pwd)/gatling-results:/opt/gatling/results \ + -e JAVA_OPTS="-Dusers=${GATLING_USERS} -Dramp=${GATLING_RAMP} -DbaseUrl=${GATLING_BASE_URL}" \ + --network=proxy \ + denvazh/gatling -m -s ghost.GhostFrontend + +# DOCKER related commands +### + +cli-version: + docker exec -it ${NAME} ghost -v + +ps: + # A lightly formatted version of docker ps + docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}} ago' + +shell: + docker exec --user node -it ${NAME} bash + +logs: + docker logs -f ${NAME} + +stop: + docker stop ${NAME} + +pull: + docker pull ghost:1-alpine + +restart: stop qa logs +restart-prod: stop prod logs + +upgrade: pull restart +upgrade-prod: pull restart-prod + + +# RELEASE PROCESS related commands +### + +app-version: + @echo VERSION set to $(VERSION) + +release: + # make sure we are in master + python update_release.py check --branch=master + + # update versions and ask for confirmation + python update_release.py + python update_release.py confirm + + # create branch and tag + git checkout -b release-$(VERSION) + git add . + git commit -m "Prepared release $(VERSION)" + git push --set-upstream origin release-$(VERSION) + + git tag $(VERSION) + git tag -f qa-release + git push --tags --force + + # updating CHANGELOG + make update-changelog + + # create github release + python update_release.py publish + + # cancel pre-update of versions + git checkout versions.py + + # git merge master + git checkout master + git merge release-$(VERSION) + git push + +push-qa: + # update tags + git tag -f qa-release + git push --tags --force + + # updating CHANGELOG + make update-changelog + +push-prod: + @# confirm push to production + @python update_release.py confirm --prod + + # update tags + git tag -f prod-release + git push --tags --force + + # updating CHANGELOG + make update-changelog + +update-changelog: + # updating CHANGELOG + github_changelog_generator + + # commit master + git add CHANGELOG.md + git commit -m "updated CHANGELOG" + git push diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..9fb3b67 --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] +requests = "*" + +[requires] +python_version = "3.6" diff --git a/update_release.py b/update_release.py new file mode 100755 index 0000000..fca244e --- /dev/null +++ b/update_release.py @@ -0,0 +1,232 @@ +#!/usr/bin/python +"""Release manager script + +This file should be used as post-commit in .git/hooks +It can be run with both python 2.7 and 3.6 + +Usage: + commands.py [-q | -d] + commands.py confirm [-q | -d] + commands.py publish [-q | -d] + commands.py check [--branch=BRANCH] [-q | -d] + commands.py -h + commands.py -v + +Options: + -h, --help display this message and exit + -v, --version display version + --branch=BRANCH branch name to check for (default to master) + -q, --quiet set log level to WARNING (instead of INFO) + -d, --debug set log level to DEBUG (instead of INFO) +""" +import os +import json +import logging +import subprocess +import sys +import shutil +import argparse +from pprint import pprint + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +COPY_PATH = os.path.sep.join([BASE_DIR, 'src', 'myapp', 'versions.py']) + +if BASE_DIR.endswith('hooks'): + BASE_DIR = os.path.abspath(os.path.sep.join( + [BASE_DIR, '..', '..'])) + sys.path.append(BASE_DIR) + +from versions import __file__ as RELEASE_FILE # noqa +from versions import _release, _build, _version # noqa + + +def set_logging_config(quiet=False, debug=False): + """ + Set logging with the 'good' level + + Arguments keywords: + kwargs -- list containing parameters passed to script + """ + # set up level of logging + level = logging.INFO + if quiet: + level = logging.WARNING + elif debug: + level = logging.DEBUG + + # set up logging to console + logging.basicConfig(format='%(levelname)s - %(funcName)s - %(message)s') + logger = logging.getLogger() + logger.setLevel(level) + + +def compute(): + logging.debug("Updating versions...") + + # compute version from release + try: + # get last release & version + release_version = list(map(int, _release.split('-')[0].split('.'))) + candidate_version = list(map(int, _version.split('-')[0].split('.'))) + # nothing to do if candidate_version is already more than release + if candidate_version > release_version: + logging.debug("version already set to %s. not changing it", candidate_version) + version = _version + # increment and convert back in strings, with suffic -rc + else: + logging.debug("incrementint version from release %s", release_version) + major, minor, patch = release_version + version = '.'.join(map(str, [major, minor, patch+1])) + "-rc" + except Exception as err: + logging.error("Exception occured while trying to generate new version: %s", err) + version = _version + + logging.info("will set _version=%s", version) + + # get build and release numbers from git + try: + build = subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=BASE_DIR).decode('utf-8').strip() + + release = subprocess.check_output( + ["git", "describe", "--tags", "--match", "[0-9]*"], cwd=BASE_DIR).decode('utf-8').strip() + except Exception as err: + logging.warning("Using previous build & release, since git does not seem available: %s", err) + release = _release + build = _build + + logging.info("will set _build=%s", build) + logging.info("will set _release=%s", release) + + # update file + with open(RELEASE_FILE, 'w') as output: + content = """# flake8: noqa +# This file is autognerated by post-commit hook + +# the release comes from git and should not be modified +# => read-only +_release = '{0}' + +# you can set the next version number manually +# if you do not, the system will make sure that version > release +# => read-write, >_release +_version = '{1}' + +# the build number will generate conflicts on each PR merge +# just keep yours every time +# => read-only +_build = '{2}'""".format(release, version, build) + output.write(content) + + +try: input = raw_input +except: pass + + +def confirm_release(): + print("version currently set to {}".format(_version)) + answer = "" + while answer not in ["y", "n"]: + answer = input("OK to push to continue [Y/N]? ").lower() + if answer != "y": + raise SystemExit("Please confirm version number to continue") + + +def confirm_push(): + try: + branch = subprocess.check_output( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=BASE_DIR).decode('utf-8').strip() + print("\nYou are on branch '{}'".format(branch)) + print("on version '{}'".format(_version)) + print("\nType [y] to comfirm push".format(branch)) + answer = input("or any other key to abort:").lower() + if answer != "y": + raise SystemExit("Push aborted...") + except Exception as err: + logging.warning("Git does not seem available: %s", err) + raise SystemExit("This command requires git") + + +def publish(dry_run=False): + """ POST /repos/:owner/:repo/releases + + https://developer.github.com/v3/repos/releases/#create-a-release + """ + # dynamic import to allow the other commands to run without requests + import requests + + # get gihub config. If not set -> POST will fail, developer will understand + github_owner = os.environ.get('GITHUB_OWNER') + github_repo = os.environ.get('GITHUB_REPO') + github_user = os.environ.get('GITHUB_USER') + github_key = os.environ.get('GITHUB_KEY') + + # build request + url = "https://api.github.com/repos/{}/{}/releases".format(github_owner, github_repo) + changelog_url = "https://github.com/{}/{}/blob/release-{}/CHANGELOG.md".format(github_owner, github_repo, _version) + post_args = { + "tag_name": _version, + "name": "Release {}".format(_version), + "body": "See [CHANGELOG.md]({}) for all details".format(changelog_url), + "draft": False, + "prerelease": False + } + logging.debug("POST %s with data: %s", url, post_args) + + # make request and raise exception if we had an issue + if not dry_run: + response = requests.post(url, data=json.dumps(post_args), auth=(github_user, github_key)) + response.raise_for_status() + else: + print("POST {}".format(url)) + print("auth({}, xxx)".format(github_user)) + pprint(post_args) + + + +def check_branch(expected='master'): + try: + current = subprocess.check_output( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=BASE_DIR).decode('utf-8').strip() + if current != expected: + raise SystemExit("You are in {}, whereas expected branch is {}".format(current, expected)) + logging.info("You are in {}".format(current)) + except Exception as err: + logging.warning("Git does not seem available: %s", err) + raise SystemExit("This command requires git") + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + usage="""Release manager script + +This file should be used as post-commit in .git/hooks +It can be run with both python 2.7 and 3.6""") + parser.add_argument("command", nargs='?', help="[confirm|publish|check]") + parser.add_argument('--prod', action='store_true', help="used with command confirm") + parser.add_argument('--dry-run', action='store_true', help="used with command publish") + parser.add_argument('--branch', help="used with command check_branch") + parser.add_argument('-v', '--version', action='store_true') + parser.add_argument('-d', '--debug', action='store_true') + parser.add_argument('-q', '--quiet', action='store_true') + args = parser.parse_args() + + set_logging_config(quiet=args.quiet, debug=args.debug) + logging.debug(args) + + # version needs to be print to output in order to be retrieved by Makefile + if args.version: + print(_version) + raise SystemExit() + + if args.command == 'confirm': + if args.prod: + confirm_push() + else: + confirm_release() + elif args.command == 'check': + check_branch(expected=args.branch) + elif args.command == 'publish': + publish(dry_run=args.dry_run) + else: + compute() diff --git a/versions.py b/versions.py new file mode 100644 index 0000000..28c1041 --- /dev/null +++ b/versions.py @@ -0,0 +1,16 @@ +# flake8: noqa +# This file is autognerated by post-commit hook + +# the release comes from git and should not be modified +# => read-only +_release = '0.2.0-11-g0620b66' + +# you can set the next version number manually +# if you do not, the system will make sure that version > release +# => read-write, >_release +_version = '0.2.1-rc' + +# the build number will generate conflicts on each PR merge +# just keep yours every time +# => read-only +_build = '0620b6603ddcb0235fa2207671e85f247ac69697' \ No newline at end of file