Update bump_build_tag.py for Xcode Cloud
This commit is contained in:
parent
e579f4c4da
commit
6e44d07800
@ -1,435 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
import argparse
|
||||
import inspect
|
||||
import feature_flags_common
|
||||
from datetime import date
|
||||
import plistlib
|
||||
import subprocess
|
||||
|
||||
INFO_PLIST_PATHS = [
|
||||
"Signal/Signal-Info.plist",
|
||||
"SignalShareExtension/Info.plist",
|
||||
"SignalNSE/Info.plist",
|
||||
]
|
||||
|
||||
|
||||
def fail(message):
|
||||
file_name = __file__
|
||||
current_line_no = inspect.stack()[1][2]
|
||||
current_function_name = inspect.stack()[1][3]
|
||||
print("Failure in:", file_name, current_line_no, current_function_name)
|
||||
print(message)
|
||||
sys.exit(1)
|
||||
def run(args):
|
||||
subprocess.run(args, check=True)
|
||||
|
||||
|
||||
def execute_command(command):
|
||||
try:
|
||||
print(" ".join(command))
|
||||
output = subprocess.check_output(command, text=True)
|
||||
if output:
|
||||
print(output)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e.output)
|
||||
sys.exit(1)
|
||||
def capture(args):
|
||||
return subprocess.run(args, check=True, capture_output=True, encoding="utf8").stdout
|
||||
|
||||
|
||||
def find_project_root():
|
||||
path = os.path.abspath(os.curdir)
|
||||
|
||||
while True:
|
||||
# print 'path', path
|
||||
if not os.path.exists(path):
|
||||
break
|
||||
git_path = os.path.join(path, ".git")
|
||||
if os.path.exists(git_path):
|
||||
return path
|
||||
new_path = os.path.abspath(os.path.dirname(path))
|
||||
if not new_path or new_path == path:
|
||||
break
|
||||
path = new_path
|
||||
|
||||
fail("Could not find project root path")
|
||||
|
||||
|
||||
def is_valid_version_1(value):
|
||||
regex = re.compile(r"^(\d+)$")
|
||||
match = regex.search(value)
|
||||
return match is not None
|
||||
|
||||
|
||||
def is_valid_version_3(value):
|
||||
regex = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
|
||||
match = regex.search(value)
|
||||
return match is not None
|
||||
|
||||
|
||||
def is_valid_version_4(value):
|
||||
regex = re.compile(r"^(\d+)\.(\d+)\.(\d+).(\d+)$")
|
||||
match = regex.search(value)
|
||||
return match is not None
|
||||
|
||||
|
||||
def set_versions(plist_file_path, release_version, build_version_1, build_version_4):
|
||||
if not is_valid_version_3(release_version):
|
||||
fail("Invalid release version: %s" % release_version)
|
||||
if not is_valid_version_1(build_version_1):
|
||||
fail("Invalid build version 1: %s" % build_version_1)
|
||||
if not is_valid_version_4(build_version_4):
|
||||
fail("Invalid build version 4: %s" % build_version_4)
|
||||
|
||||
with open(plist_file_path, "rt") as f:
|
||||
text = f.read()
|
||||
# print 'text', text
|
||||
|
||||
# CFBundleShortVersionString is the release version.
|
||||
#
|
||||
# <key>CFBundleShortVersionString</key>
|
||||
# <string>2.20.0</string>
|
||||
file_regex = re.compile(
|
||||
r"<key>CFBundleShortVersionString</key>\s*<string>([\d\.]+)</string>",
|
||||
re.MULTILINE,
|
||||
)
|
||||
file_match = file_regex.search(text)
|
||||
# print 'match', match
|
||||
if not file_match:
|
||||
fail("Could not parse .plist")
|
||||
text = text[: file_match.start(1)] + release_version + text[file_match.end(1) :]
|
||||
|
||||
# CFBundleVersion is the build version 1.
|
||||
#
|
||||
# <key>CFBundleVersion</key>
|
||||
# <string>3</string>
|
||||
file_regex = re.compile(
|
||||
r"<key>CFBundleVersion</key>\s*<string>([\d\.]+)</string>", re.MULTILINE
|
||||
)
|
||||
file_match = file_regex.search(text)
|
||||
# print 'match', match
|
||||
if not file_match:
|
||||
fail("Could not parse .plist")
|
||||
text = text[: file_match.start(1)] + build_version_1 + text[file_match.end(1) :]
|
||||
|
||||
with open(plist_file_path, "wt") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
# Represents a version string with 1 values, e.g. 1.
|
||||
class Version1:
|
||||
def __init__(self, build):
|
||||
self.build = build
|
||||
|
||||
def formatted(self):
|
||||
return str(self.build)
|
||||
|
||||
|
||||
# Represents a version string with 3 dotted values, e.g. 1.2.3.
|
||||
class Version3:
|
||||
class Version:
|
||||
def __init__(self, major, minor, patch):
|
||||
self.major = major
|
||||
self.minor = minor
|
||||
self.patch = patch
|
||||
|
||||
def formatted(self):
|
||||
return str(self.major) + "." + str(self.minor) + "." + str(self.patch)
|
||||
def pretty(self):
|
||||
return f"{self.major}.{self.minor}.{self.patch}"
|
||||
|
||||
def pretty2(self):
|
||||
assert self.patch == 0
|
||||
return f"{self.major}.{self.minor}"
|
||||
|
||||
|
||||
# Represents a version string with 4 dotted values, e.g. 1.2.3.4.
|
||||
class Version4:
|
||||
def __init__(self, major, minor, patch, build):
|
||||
self.major = major
|
||||
self.minor = minor
|
||||
self.patch = patch
|
||||
self.build = build
|
||||
|
||||
def formatted(self):
|
||||
return (
|
||||
str(self.major)
|
||||
+ "."
|
||||
+ str(self.minor)
|
||||
+ "."
|
||||
+ str(self.patch)
|
||||
+ "."
|
||||
+ str(self.build)
|
||||
)
|
||||
|
||||
def asVersion3(self):
|
||||
return Version3(self.major, self.minor, self.patch)
|
||||
def parse_version(value):
|
||||
components = list(map(int, value.split(".")))
|
||||
assert len(components) in (2, 3)
|
||||
while len(components) < 3:
|
||||
components.append(0)
|
||||
return Version(components[0], components[1], components[2])
|
||||
|
||||
|
||||
def parse_version_4(text):
|
||||
# print 'text', text
|
||||
regex = re.compile(r"^(\d+)\.(\d+)\.(\d+)\.?(\d+)?$")
|
||||
match = regex.search(text)
|
||||
# print 'match', match
|
||||
if not match:
|
||||
fail("Could not parse .plist")
|
||||
if len(match.groups()) < 3 or len(match.groups()) > 4:
|
||||
fail("Could not parse .plist")
|
||||
major = int(match.group(1))
|
||||
minor = int(match.group(2))
|
||||
patch = int(match.group(3))
|
||||
if match.group(4) != None:
|
||||
build = int(match.group(4))
|
||||
else:
|
||||
build = 0
|
||||
|
||||
version = Version4(major, minor, patch, build)
|
||||
# Verify that roundtripping yields the same value (or a version3 equivalent)
|
||||
if version.formatted() != text and version.asVersion3().formatted() != text:
|
||||
fail("Could not parse .plist")
|
||||
|
||||
return version
|
||||
|
||||
|
||||
def parse_version_1(text):
|
||||
build = int(text)
|
||||
|
||||
version = Version1(build)
|
||||
|
||||
# Verify that roundtripping yields the same value.
|
||||
if version.formatted() != text:
|
||||
fail("Could not parse .plist")
|
||||
|
||||
return version
|
||||
|
||||
|
||||
def get_versions(plist_file_path):
|
||||
with open(plist_file_path, "rt") as f:
|
||||
text = f.read()
|
||||
# print 'text', text
|
||||
|
||||
# CFBundleShortVersionString identifies the release track.
|
||||
# CFBundleVersion uniqely identifies the build within the release track.
|
||||
#
|
||||
# Previously, we used version strings like this:
|
||||
#
|
||||
# <key>CFBundleShortVersionString</key>
|
||||
# <string>2.13.0</string>
|
||||
# <key>CFBundleVersion</key>
|
||||
# <string>2.13.0.13</string>
|
||||
#
|
||||
# We now use version strings like this:
|
||||
#
|
||||
# <key>CFBundleShortVersionString</key>
|
||||
# <string>2.13.0</string>
|
||||
# <key>CFBundleVersion</key>
|
||||
# <string>13</string>
|
||||
#
|
||||
# See:
|
||||
#
|
||||
# * https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring
|
||||
# * https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleversion
|
||||
# * https://developer.apple.com/library/archive/technotes/tn2420/_index.html
|
||||
release_version_regex = re.compile(
|
||||
r"<key>CFBundleShortVersionString</key>\s*<string>(\d+\.\d+\.\d+)</string>",
|
||||
re.MULTILINE,
|
||||
)
|
||||
release_version_match = release_version_regex.search(text)
|
||||
# print 'match', match
|
||||
if not release_version_match:
|
||||
fail("Could not parse .plist")
|
||||
|
||||
build_version_1_regex = re.compile(
|
||||
r"<key>CFBundleVersion</key>\s*<string>(\d+)</string>", re.MULTILINE
|
||||
)
|
||||
build_version_1_match = build_version_1_regex.search(text)
|
||||
# print 'match', match
|
||||
if not build_version_1_match:
|
||||
fail("Could not parse .plist")
|
||||
|
||||
release_version_str = release_version_match.group(1)
|
||||
print("CFBundleShortVersionString:", release_version_str)
|
||||
release_version = parse_version_4(release_version_str).asVersion3()
|
||||
print("old_release_version:", release_version.formatted())
|
||||
|
||||
build_version_1_str = build_version_1_match.group(1)
|
||||
print("CFBundleVersion:", build_version_1_str)
|
||||
build_version_1 = parse_version_1(build_version_1_str)
|
||||
print("old_build_version_1:", build_version_1.formatted())
|
||||
|
||||
return release_version, build_version_1
|
||||
|
||||
|
||||
def get_tag_variant(args):
|
||||
is_internal = args.internal
|
||||
is_nightly = args.nightly
|
||||
is_beta = args.beta
|
||||
|
||||
argument_tag = ""
|
||||
if is_internal:
|
||||
argument_tag = "internal"
|
||||
elif is_nightly:
|
||||
argument_tag = "nightly"
|
||||
elif is_beta:
|
||||
argument_tag = "beta"
|
||||
|
||||
current_flag = feature_flags_common.get_feature_flag()
|
||||
|
||||
# Some of these flags are legacy.
|
||||
if current_flag in ["internal"]:
|
||||
feature_flag_tag = "internal"
|
||||
elif current_flag in ["beta"]:
|
||||
feature_flag_tag = "beta"
|
||||
elif current_flag in ["production"]:
|
||||
feature_flag_tag = ""
|
||||
else:
|
||||
print("Unrecognized feature flag: " + current_flag)
|
||||
feature_flag_tag = None
|
||||
|
||||
if is_nightly or feature_flag_tag == None:
|
||||
# Just trust the tag variant specified via argument if:
|
||||
# - It's a nightly build. Those are automated and we shouldn't bug a script with interactive input requests
|
||||
# - We don't recognize the build variant.
|
||||
return argument_tag
|
||||
elif argument_tag == feature_flag_tag:
|
||||
return argument_tag
|
||||
else:
|
||||
# A mismatch! Let's check with the user to see if they really wanted
|
||||
# a tag variant that matched the current feature flag.
|
||||
argument_tag_string = argument_tag if len(argument_tag) > 0 else "production"
|
||||
feature_flag_tag_string = (
|
||||
feature_flag_tag if len(feature_flag_tag) > 0 else "production"
|
||||
)
|
||||
|
||||
print(
|
||||
"Feature flag mismatch! Arguments specify a "
|
||||
+ argument_tag_string
|
||||
+ " tag but the current feature flag indicates a "
|
||||
+ feature_flag_tag_string
|
||||
+ " tag may be more appropriate."
|
||||
)
|
||||
prefer_feature_flag = input(
|
||||
"Proceed with a " + feature_flag_tag_string + " instead? (Y/n) "
|
||||
)
|
||||
|
||||
if len(prefer_feature_flag) == 0 or prefer_feature_flag[0] in "Yy":
|
||||
return feature_flag_tag
|
||||
else:
|
||||
return argument_tag
|
||||
def set_version(path, version):
|
||||
with open(path, "rb") as file:
|
||||
contents = plistlib.load(file)
|
||||
contents["CFBundleShortVersionString"] = version.pretty()
|
||||
with open(path, "wb") as file:
|
||||
plistlib.dump(contents, file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Precommit cleanup script.")
|
||||
parser.add_argument("--version", help="used for starting a new version.")
|
||||
|
||||
type_group = parser.add_mutually_exclusive_group()
|
||||
type_group.add_argument(
|
||||
"--internal", action="store_true", help="used to indicate throwaway builds."
|
||||
parser = argparse.ArgumentParser(description="bumps the marketing version")
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
metavar="x.y.z",
|
||||
required=True,
|
||||
help="specify the new marketing version number",
|
||||
)
|
||||
type_group.add_argument(
|
||||
"--nightly", action="store_true", help="used to indicate nightly builds."
|
||||
)
|
||||
type_group.add_argument(
|
||||
"--beta", action="store_true", help="used to indicate beta builds."
|
||||
parser.add_argument(
|
||||
"--nightly", action="store_true", help="specify that this is a nightly build"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
tag_variant = get_tag_variant(args)
|
||||
ns = parser.parse_args()
|
||||
|
||||
project_root_path = find_project_root()
|
||||
# print 'project_root_path', project_root_path
|
||||
# plist_path
|
||||
main_plist_path = os.path.join(project_root_path, "Signal", "Signal-Info.plist")
|
||||
if not os.path.exists(main_plist_path):
|
||||
fail("Could not find main app info .plist")
|
||||
|
||||
sae_plist_path = os.path.join(
|
||||
project_root_path, "SignalShareExtension", "Info.plist"
|
||||
)
|
||||
if not os.path.exists(sae_plist_path):
|
||||
fail("Could not find share extension info .plist")
|
||||
|
||||
nse_plist_path = os.path.join(project_root_path, "SignalNSE", "Info.plist")
|
||||
if not os.path.exists(nse_plist_path):
|
||||
fail("Could not find NSE info .plist")
|
||||
|
||||
output = subprocess.check_output(["git", "status", "--porcelain"], text=True)
|
||||
if len(output.strip()) > 0:
|
||||
output = capture(["git", "status", "--porcelain"]).rstrip()
|
||||
if len(output) > 0:
|
||||
print(output)
|
||||
fail("Git repository has untracked files.")
|
||||
output = subprocess.check_output(["git", "diff", "--shortstat"], text=True)
|
||||
if len(output.strip()) > 0:
|
||||
print(output)
|
||||
fail("Git repository has untracked files.")
|
||||
print("Repository has uncommitted changes.")
|
||||
exit(1)
|
||||
|
||||
# Ensure .plist is in xml format, not binary.
|
||||
plist_paths = [
|
||||
main_plist_path,
|
||||
sae_plist_path,
|
||||
nse_plist_path,
|
||||
]
|
||||
for plist_path in plist_paths:
|
||||
print("plist_path:", plist_path)
|
||||
version = parse_version(ns.version)
|
||||
|
||||
output = subprocess.check_output(
|
||||
["plutil", "-convert", "xml1", plist_path], text=True
|
||||
)
|
||||
# print 'output', output
|
||||
for path in INFO_PLIST_PATHS:
|
||||
set_version(path, version)
|
||||
|
||||
# ---------------
|
||||
# Main App
|
||||
# ---------------
|
||||
|
||||
old_release_version, old_build_version_1 = get_versions(main_plist_path)
|
||||
|
||||
if args.version:
|
||||
# Update version to the provided argument
|
||||
# e.g. --version 1.2.3 -> "1.2.3", "0"
|
||||
# e.g. --version 1.2.3.4 -> "1.2.3" "4"
|
||||
new_build_version_4 = parse_version_4(args.version.strip())
|
||||
new_build_version_1 = Version1(new_build_version_4.build)
|
||||
new_release_version_3 = new_build_version_4.asVersion3()
|
||||
# print 'new_release_version_3:', new_release_version_3.formatted()
|
||||
|
||||
else:
|
||||
# Bump patch.
|
||||
new_release_version_3 = old_release_version
|
||||
new_build_version_1 = Version1(old_build_version_1.build + 1)
|
||||
new_build_version_4 = Version4(
|
||||
new_release_version_3.major,
|
||||
new_release_version_3.minor,
|
||||
new_release_version_3.patch,
|
||||
old_build_version_1.build + 1,
|
||||
)
|
||||
|
||||
new_release_version_3 = new_release_version_3.formatted()
|
||||
new_build_version_1 = new_build_version_1.formatted()
|
||||
new_build_version_4 = new_build_version_4.formatted()
|
||||
|
||||
# For example:
|
||||
#
|
||||
# old_release_version: 5.19.0
|
||||
# old_build_version_1: 42
|
||||
# new_release_version_3: 5.19.0
|
||||
# new_build_version_1: 43
|
||||
# new_build_version_4: 5.19.0.43
|
||||
print("new_release_version_3:", new_release_version_3)
|
||||
print("new_build_version_1:", new_build_version_1)
|
||||
print("new_build_version_4:", new_build_version_4)
|
||||
|
||||
for plist_path in plist_paths:
|
||||
set_versions(
|
||||
plist_path, new_release_version_3, new_build_version_1, new_build_version_4
|
||||
)
|
||||
|
||||
# ---------------
|
||||
# Git
|
||||
# ---------------
|
||||
command = ["git", "add", "."]
|
||||
execute_command(command)
|
||||
|
||||
if tag_variant == "internal":
|
||||
commit_message = '"Bump build to %s." (Internal)' % new_build_version_4
|
||||
elif tag_variant == "beta":
|
||||
commit_message = '"Bump build to %s." (Beta)' % new_build_version_4
|
||||
elif tag_variant == "nightly":
|
||||
commit_message = '"Bump build to %s." (nightly-%s)' % (
|
||||
new_build_version_4,
|
||||
date.today().strftime("%m-%d-%Y"),
|
||||
)
|
||||
else:
|
||||
commit_message = '"Bump build to %s."' % new_build_version_4
|
||||
command = ["git", "commit", "-m", commit_message]
|
||||
execute_command(command)
|
||||
|
||||
tag_name = new_build_version_4
|
||||
if len(tag_variant) > 0:
|
||||
tag_name += "-" + tag_variant
|
||||
|
||||
command = ["git", "tag", tag_name]
|
||||
execute_command(command)
|
||||
run(["git", "add", *INFO_PLIST_PATHS])
|
||||
run(["git", "commit", "-m", f"Bump version to {version.pretty()}"])
|
||||
if version.patch == 0:
|
||||
run(["git", "tag", f"version-{version.pretty2()}"])
|
||||
|
||||
@ -36,15 +36,6 @@ extension FeatureBuild {
|
||||
)
|
||||
|
||||
|
||||
def extract(value):
|
||||
return value.split("\n")[11][40:]
|
||||
|
||||
|
||||
def get_feature_flag():
|
||||
with open(FILE_PATH, "r") as file:
|
||||
return extract(file.read())
|
||||
|
||||
|
||||
def set_feature_flags(new_flags_level):
|
||||
output = capture(["git", "status", "--porcelain"]).rstrip()
|
||||
if len(output) > 0:
|
||||
@ -53,7 +44,6 @@ def set_feature_flags(new_flags_level):
|
||||
exit(1)
|
||||
|
||||
new_value = generate(new_flags_level)
|
||||
assert extract(new_value) == str(new_flags_level)
|
||||
with open(FILE_PATH, "r") as file:
|
||||
old_value = file.read()
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>4</string>
|
||||
<string>0</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LOGS_EMAIL</key>
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>7.8.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>4</string>
|
||||
<string>0</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSExceptionDomains</key>
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>7.8.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>4</string>
|
||||
<string>0</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user