|
| 1 | +''' |
| 2 | +Utility to schedule MAS builds in Bitrise. |
| 3 | +
|
| 4 | +Examples: |
| 5 | +
|
| 6 | +- Publish a snapshot from master (release.py uses the current branch) |
| 7 | +
|
| 8 | + $ git branch |
| 9 | + * master |
| 10 | + 1234-fix-crash |
| 11 | + $ python release.py --stage snapshot |
| 12 | +
|
| 13 | +- Publish a snapshot from a feature branch (same as before, just switch branchs with git): |
| 14 | +
|
| 15 | + $ git branch |
| 16 | + master |
| 17 | + * 1234-fix-crash |
| 18 | + $ python release.py --stage snapshot |
| 19 | +
|
| 20 | +- Publish a beta from a pre-release branch: |
| 21 | +
|
| 22 | + $ git branch |
| 23 | + master |
| 24 | + * release-android-420-beta1 |
| 25 | + $ python release.py --stage beta --version 2.0.0-beta.1 |
| 26 | +
|
| 27 | +- Publish a beta from a release branch: |
| 28 | +
|
| 29 | + $ git branch |
| 30 | + master |
| 31 | + * release-android-420 |
| 32 | + $ python release.py --stage final --version 2.0.0 |
| 33 | +
|
| 34 | +TODO: |
| 35 | +
|
| 36 | +- Add a flag to wait until the release has been built (Bitrise) and published (Maven). |
| 37 | +
|
| 38 | +''' |
| 39 | + |
| 40 | +import click |
| 41 | +import json |
| 42 | +import os |
| 43 | +import requests |
| 44 | +import subprocess |
| 45 | + |
| 46 | +# Three stages, from less stable to more stable |
| 47 | +ALLOWED_STAGES = ['snapshot', 'beta', 'final'] |
| 48 | + |
| 49 | +# Get the version from GRADLE_PROPERTIES_PATH below |
| 50 | +CURRENT_VERSION_TAG = 'current' |
| 51 | + |
| 52 | +# You can find the API token in https://www.bitrise.io/app/a7eea7d04be1e2e5#/code -> API token |
| 53 | +BITRISE_API_TOKEN_ENV_VAR = 'BITRISE_API_TOKEN_MAS' |
| 54 | + |
| 55 | +# In the future we might want to consider alpha, or rc. |
| 56 | +ALLOWED_PRE_RELEASE = ['beta'] |
| 57 | + |
| 58 | +# We get the default version from here |
| 59 | +MAPBOX_GL_ANDROID_SDK_PATH = '../mapbox' |
| 60 | +GRADLE_PROPERTIES_PATH = '%s/gradle.properties' % MAPBOX_GL_ANDROID_SDK_PATH |
| 61 | +GRADLE_TOKEN = 'VERSION_NAME=' |
| 62 | + |
| 63 | +# Bitrise |
| 64 | +URL_BITRISE = 'https://www.bitrise.io/app/a7eea7d04be1e2e5/build/start.json' |
| 65 | + |
| 66 | +# We support three parameters: stage, branch, and version |
| 67 | +@click.command() |
| 68 | +@click.option('--stage', default=ALLOWED_STAGES[0], type=click.Choice(ALLOWED_STAGES), prompt='Set stage', help='The release stage.') |
| 69 | +@click.option('--version', default=CURRENT_VERSION_TAG, prompt='Set version', help='The version you want to publish. E.g: 2.0.0-SNAPSHOT, 2.0.0-beta.1, or 2.0.0. If you set the version to "%s", the script will default to the current SNAPSHOT version.' % CURRENT_VERSION_TAG) |
| 70 | +def release(stage, version): |
| 71 | + # Validate params |
| 72 | + final_stage = validate_stage(stage=stage) |
| 73 | + final_branch = validate_branch(stage=final_stage) |
| 74 | + final_version = validate_version(stage=final_stage, branch=final_branch, version=version) |
| 75 | + |
| 76 | + # Get user confirmation |
| 77 | + click.echo('\n===== Build information =====') |
| 78 | + click.echo('- Stage: %s' % final_stage) |
| 79 | + click.echo('- Branch: %s' % final_branch) |
| 80 | + click.echo('- Version: %s' % final_version) |
| 81 | + click.confirm('\nDoes it look right?', abort=True) |
| 82 | + |
| 83 | + # Proceed |
| 84 | + if (final_stage == 'snapshot'): |
| 85 | + publish_snapshot(branch=final_branch, version=final_version) |
| 86 | + elif (final_stage == 'beta'): |
| 87 | + publish_beta(branch=final_branch, version=final_version) |
| 88 | + elif (final_stage == 'final'): |
| 89 | + publish_final(branch=final_branch, version=final_version) |
| 90 | + |
| 91 | +def validate_stage(stage): |
| 92 | + if stage not in ALLOWED_STAGES: |
| 93 | + abort_with_message('Invalid stage: %s' % stage) |
| 94 | + return stage |
| 95 | + |
| 96 | +def validate_branch(stage): |
| 97 | + branch = git_get_current_branch() |
| 98 | + if not branch: |
| 99 | + abort_with_message('The current folder is not a git repository.') |
| 100 | + if branch == 'master' and stage != 'snapshot': |
| 101 | + abort_with_message('You need to swtich to a release branch for a beta or a final release.') |
| 102 | + return branch |
| 103 | + |
| 104 | +def validate_version(stage, branch, version): |
| 105 | + if stage == 'snapshot' and branch == 'master' and version != CURRENT_VERSION_TAG: |
| 106 | + abort_with_message('You cannot specify a custom version if you are building a snapshot from master.') |
| 107 | + |
| 108 | + if not version or version == CURRENT_VERSION_TAG: |
| 109 | + version = get_current_version(file_path=GRADLE_PROPERTIES_PATH, file_var=GRADLE_TOKEN) |
| 110 | + |
| 111 | + if stage == 'snapshot': |
| 112 | + if not 'SNAPSHOT' in version: |
| 113 | + abort_with_message('Version should contain the word SNAPSHOT: %s' % version) |
| 114 | + elif stage == 'beta': |
| 115 | + if not ALLOWED_PRE_RELEASE[0] in version: |
| 116 | + abort_with_message('Version should contain the word %s: %s' % (ALLOWED_PRE_RELEASE[0], version)) |
| 117 | + elif stage == 'final': |
| 118 | + if not version or 'SNAPSHOT' in version or ALLOWED_PRE_RELEASE[0] in version: |
| 119 | + abort_with_message('Version cannot be empty, or contain the words SNAPSHOT or %s: %s' % (ALLOWED_PRE_RELEASE[0], version)) |
| 120 | + |
| 121 | + return version |
| 122 | + |
| 123 | +def publish_snapshot(branch, version): |
| 124 | + click.echo('Publishing snapshot for branch: %s (version: %s).' % (branch, version)) |
| 125 | + if branch != 'master': |
| 126 | + dirty_gradle = update_current_version(file_path=GRADLE_PROPERTIES_PATH, file_var=GRADLE_TOKEN, version=version) |
| 127 | + if dirty_gradle: |
| 128 | + git_add(path=GRADLE_PROPERTIES_PATH) |
| 129 | + git_commit_and_push(branch=branch, version=version) |
| 130 | + do_bitrise_request(build_params={'branch': branch, 'workflow_id': 'scheduled'}) |
| 131 | + |
| 132 | +def publish_beta(branch, version): |
| 133 | + click.echo('Publishing beta from branch: %s (version: %s).' % (branch, version)) |
| 134 | + dirty_gradle = update_current_version(file_path=GRADLE_PROPERTIES_PATH, file_var=GRADLE_TOKEN, version=version) |
| 135 | + if dirty_gradle: |
| 136 | + git_add(path=GRADLE_PROPERTIES_PATH) |
| 137 | + git_commit_and_push(branch=branch, version=version) |
| 138 | + do_bitrise_request(build_params={'branch': branch, 'workflow_id': 'scheduled'}) |
| 139 | + |
| 140 | +def publish_final(branch, version): |
| 141 | + click.echo('Publishing final release from branch: %s (version: %s).' % (branch, version)) |
| 142 | + dirty_gradle = update_current_version(file_path=GRADLE_PROPERTIES_PATH, file_var=GRADLE_TOKEN, version=version) |
| 143 | + if dirty_gradle: |
| 144 | + git_add(path=GRADLE_PROPERTIES_PATH) |
| 145 | + git_commit_and_push(branch=branch, version=version) |
| 146 | + do_bitrise_request(build_params={'branch': branch, 'workflow_id': 'scheduled'}) |
| 147 | + |
| 148 | +# |
| 149 | +# Utils |
| 150 | +# |
| 151 | + |
| 152 | +def abort_with_message(message): |
| 153 | + click.echo(message) |
| 154 | + click.get_current_context().abort() |
| 155 | + |
| 156 | +def execute_call(command): |
| 157 | + click.echo('Executing: %s' % command) |
| 158 | + result = subprocess.call(command, shell=True) |
| 159 | + if result != 0: |
| 160 | + abort_with_message('Command failed: %s' % command) |
| 161 | + |
| 162 | +# |
| 163 | +# Bitrise |
| 164 | +# |
| 165 | + |
| 166 | +def get_bitrise_api_token(): |
| 167 | + bitrise_api_token = os.environ.get(BITRISE_API_TOKEN_ENV_VAR) |
| 168 | + if not bitrise_api_token: |
| 169 | + abort_with_message('You need to set the BITRISE_API_TOKEN_MAS environment variable.') |
| 170 | + click.echo('Found Bitrise API token.') |
| 171 | + return bitrise_api_token |
| 172 | + |
| 173 | +def do_bitrise_request(build_params): |
| 174 | + data = { |
| 175 | + 'hook_info': {'type': 'bitrise', 'api_token': get_bitrise_api_token()}, |
| 176 | + 'build_params' : build_params} |
| 177 | + click.echo('Bitrise request data: %s' % json.dumps(data)) |
| 178 | + click.confirm('\nDo you want to start a build?', abort=True) |
| 179 | + |
| 180 | + r = requests.post(URL_BITRISE, data=json.dumps(data)) |
| 181 | + click.echo('- Bitrise response code: %s' % r.status_code) |
| 182 | + click.echo('- Bitrise response content: %s' % r.text) |
| 183 | + |
| 184 | +# |
| 185 | +# Git |
| 186 | +# |
| 187 | + |
| 188 | +def git_get_current_branch(): |
| 189 | + return subprocess.check_output('git symbolic-ref --short HEAD'.split(' ')).strip() |
| 190 | + |
| 191 | +def git_add(path): |
| 192 | + execute_call(command='git add %s' % path) |
| 193 | + |
| 194 | +def git_commit_and_push(branch, version): |
| 195 | + message = '[android] [auto] Update properties to version %s in preparation for build.' % version |
| 196 | + commands = [ |
| 197 | + 'git commit -m "%s"' % message, |
| 198 | + 'git push -u origin %s' % branch] |
| 199 | + for command in commands: |
| 200 | + execute_call(command=command) |
| 201 | + |
| 202 | +# |
| 203 | +# Read and update properties files |
| 204 | +# |
| 205 | + |
| 206 | +def get_current_version(file_path, file_var): |
| 207 | + click.echo('Getting current version from %s.' % file_path) |
| 208 | + with open(file_path, 'r') as f: |
| 209 | + for line in f: |
| 210 | + if line.startswith(file_var): |
| 211 | + version_name = line[len(file_var):].strip() |
| 212 | + click.echo('Current version is %s.' % version_name) |
| 213 | + return version_name |
| 214 | + return None |
| 215 | + |
| 216 | +def update_current_version(file_path, file_var, version): |
| 217 | + dirty = False |
| 218 | + click.echo('Updating file to version %s: %s.' % (version, file_path)) |
| 219 | + with open(file_path, 'r') as f: |
| 220 | + file_lines = f.readlines() |
| 221 | + for line_number in range(len(file_lines)): |
| 222 | + if file_lines[line_number].startswith(file_var): |
| 223 | + content_old = file_lines[line_number] |
| 224 | + content_new = '%s%s\n' % (file_var, version) |
| 225 | + if (content_old != content_new): |
| 226 | + click.echo('%s -> %s' % (content_old.strip(), content_new.strip())) |
| 227 | + file_lines[line_number] = content_new |
| 228 | + dirty = True |
| 229 | + if dirty: |
| 230 | + with open(file_path, 'w') as f: |
| 231 | + f.writelines(file_lines) |
| 232 | + else: |
| 233 | + click.echo('File already has the right version.') |
| 234 | + return dirty |
| 235 | + |
| 236 | +if __name__ == '__main__': |
| 237 | + release() |
0 commit comments