File: appservices_version_bump.py

package info (click to toggle)
firefox 147.0.4-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 4,683,532 kB
  • sloc: cpp: 7,607,356; javascript: 6,533,348; ansic: 3,775,236; python: 1,415,508; xml: 634,561; asm: 438,949; java: 186,241; sh: 62,760; makefile: 18,079; objc: 13,092; perl: 12,808; yacc: 4,583; cs: 3,846; pascal: 3,448; lex: 1,720; ruby: 1,003; php: 436; lisp: 258; awk: 247; sql: 66; sed: 54; csh: 10; exp: 6
file content (216 lines) | stat: -rwxr-xr-x 6,781 bytes parent folder | download | duplicates (12)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
#!/usr/bin/env python3

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/

# Helper script for mach vendor / updatebot to update the application-services
# version pin in ApplicationServices.kt to the latest available
# application-services nightly build.
#
# This script was adapted from https://github.com/mozilla-mobile/relbot/

import datetime
import logging
import os
import re
from urllib.parse import quote_plus

import requests
from mozbuild.vendor.host_base import BaseHost

log = logging.getLogger(__name__)
logging.basicConfig(
    format="%(asctime)s - %(name)s.%(funcName)s:%(lineno)s - %(levelname)s - %(message)s",  # noqa E501
    level=logging.INFO,
)


def get_contents(path):
    with open(path) as f:
        return f.read()


def write_contents(path, new_content):
    with open(path, "w") as f:
        f.write(new_content)


def get_app_services_version_path():
    """Return the file path to dependencies file"""
    p = "ApplicationServices.kt"
    if os.path.exists(p):
        return p
    return "mobile/android/android-components/plugins/dependencies/src/main/java/ApplicationServices.kt"


def taskcluster_indexed_artifact_url(index_name, artifact_path):
    artifact_path = quote_plus(artifact_path)
    return (
        "https://firefox-ci-tc.services.mozilla.com/"
        f"api/index/v1/task/{index_name}/artifacts/{artifact_path}"
    )


def validate_as_version(v):
    """Validate that v is in an expected format for an app-services version. Returns v or raises an exception."""

    match = re.match(r"(^\d+)\.\d+$", v)
    if match:
        # Application-services switched to following the 2-component the
        # Firefox version number in v114
        if int(match.group(1)) >= 114:
            return v
    raise Exception(f"Invalid version format {v}")


def validate_as_channel(c):
    """Validate that c is a valid app-services channel."""
    if c in ("staging", "nightly_staging"):
        # These are channels are valid, but only used for preview builds.  We don't have
        # any way of auto-updating them
        raise Exception(f"Can't update app-services channel {c}")
    if c not in ("release", "nightly"):
        raise Exception(f"Invalid app-services channel {c}")
    return c


def get_current_as_version():
    """Return the current nightly app-services version"""
    regex = re.compile(r'val VERSION = "([\d\.]+)"', re.MULTILINE)

    path = get_app_services_version_path()
    src = get_contents(path)
    match = regex.search(src)
    if match:
        return validate_as_version(match[1])
    raise Exception(
        f"Could not find application-services version in {os.path.basename(path)}"
    )


def match_as_channel(src):
    """
    Find the ApplicationServicesChannel channel in the contents of the given
    ApplicationServices.kt file.
    """
    match = re.compile(
        r"val CHANNEL = ApplicationServicesChannel."
        r"(NIGHTLY|NIGHTLY_STAGING|STAGING|RELEASE)",
        re.MULTILINE,
    ).search(src)
    if match:
        return validate_as_channel(match[1].lower())
    raise Exception("Could not match the channel in ApplicationServices.kt")


def get_current_as_channel():
    """Return the current app-services channel"""
    content = get_contents(get_app_services_version_path())
    return match_as_channel(content)


def get_as_nightly_json(version="latest"):
    r = requests.get(
        taskcluster_indexed_artifact_url(
            f"project.application-services.v2.nightly.{version}",
            "public/build/nightly.json",
        )
    )
    r.raise_for_status()
    return r.json()


def compare_as_versions(a, b):
    # Tricky cmp()-style function for application services versions.  Note that
    # this works with both 2-component versions and 3-component ones, Since
    # python compares tuples element by element.
    a = tuple(int(x) for x in validate_as_version(a).split("."))
    b = tuple(int(x) for x in validate_as_version(b).split("."))
    return (a > b) - (a < b)


def update_as_version(old_as_version, new_as_version):
    """Update the VERSION in ApplicationServices.kt"""
    path = get_app_services_version_path()
    current_version_string = f'val VERSION = "{old_as_version}"'
    new_version_string = f'val VERSION = "{new_as_version}"'
    log.info(f"Updating app-services version in {path}")

    content = get_contents(path)
    new_content = content.replace(current_version_string, new_version_string)
    if content == new_content:
        raise Exception(
            "Update to ApplicationServices.kt resulted in no changes: "
            "maybe the file was already up to date?"
        )

    write_contents(path, new_content)


def update_application_services(revision):
    """Find the app-services nightly build version corresponding to revision;
    if it is newer than the current version in ApplicationServices.kt, then
    update ApplicationServices.kt with the newer version number."""
    as_channel = get_current_as_channel()
    log.info(f"Current app-services channel is {as_channel}")
    if as_channel != "nightly":
        raise NotImplementedError(
            "Only the app-services nightly channel is currently supported"
        )

    current_as_version = get_current_as_version()
    log.info(
        f"Current app-services {as_channel.capitalize()} version is {current_as_version}"
    )

    json = get_as_nightly_json(f"revision.{revision}")
    target_as_version = json["version"]
    log.info(
        f"Target app-services {as_channel.capitalize()} version "
        f"is {target_as_version}"
    )

    if compare_as_versions(current_as_version, target_as_version) >= 0:
        log.warning(
            f"No newer app-services {as_channel.capitalize()} release found. Exiting."
        )
        return

    dry_run = os.getenv("DRY_RUN") == "True"
    if dry_run:
        log.warning("Dry-run so not continuing.")
        return

    update_as_version(
        current_as_version,
        target_as_version,
    )


class ASHost(BaseHost):
    def upstream_tag(self, revision):
        if revision == "HEAD":
            index = "latest"
        else:
            index = f"revision.{revision}"
        json = get_as_nightly_json(index)
        timestamp = json["version"].rsplit(".", 1)[1]
        return (
            json["commit"],
            datetime.datetime.strptime(timestamp, "%Y%m%d%H%M%S").isoformat(),
        )


def main():
    import sys

    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} <commit hash>", file=sys.stderr)
        sys.exit(1)

    update_application_services(sys.argv[1])


if __name__ == "__main__":
    main()