xref: /aosp_15_r20/external/executorch/exir/serde/schema_check.py (revision 523fa7a60841cd1ecfb9cc4201f1ca8b03ed023a)
1*523fa7a6SAndroid Build Coastguard Worker# Copyright (c) Meta Platforms, Inc. and affiliates.
2*523fa7a6SAndroid Build Coastguard Worker# All rights reserved.
3*523fa7a6SAndroid Build Coastguard Worker#
4*523fa7a6SAndroid Build Coastguard Worker# This source code is licensed under the BSD-style license found in the
5*523fa7a6SAndroid Build Coastguard Worker# LICENSE file in the root directory of this source tree.
6*523fa7a6SAndroid Build Coastguard Worker
7*523fa7a6SAndroid Build Coastguard Workerimport dataclasses
8*523fa7a6SAndroid Build Coastguard Workerimport hashlib
9*523fa7a6SAndroid Build Coastguard Workerimport re
10*523fa7a6SAndroid Build Coastguard Workerimport typing
11*523fa7a6SAndroid Build Coastguard Workerfrom enum import IntEnum
12*523fa7a6SAndroid Build Coastguard Workerfrom typing import Any, Dict, Optional, Union
13*523fa7a6SAndroid Build Coastguard Worker
14*523fa7a6SAndroid Build Coastguard Workerfrom torch._export.serde import schema
15*523fa7a6SAndroid Build Coastguard Workerfrom torch._export.serde.union import _Union
16*523fa7a6SAndroid Build Coastguard Worker
17*523fa7a6SAndroid Build Coastguard Worker
18*523fa7a6SAndroid Build Coastguard Workerclass SchemaUpdateError(Exception):
19*523fa7a6SAndroid Build Coastguard Worker    pass
20*523fa7a6SAndroid Build Coastguard Worker
21*523fa7a6SAndroid Build Coastguard Worker
22*523fa7a6SAndroid Build Coastguard Workerdef _check(x, msg):
23*523fa7a6SAndroid Build Coastguard Worker    if not x:
24*523fa7a6SAndroid Build Coastguard Worker        raise SchemaUpdateError(msg)
25*523fa7a6SAndroid Build Coastguard Worker
26*523fa7a6SAndroid Build Coastguard Worker
27*523fa7a6SAndroid Build Coastguard Workerdef _staged_schema():
28*523fa7a6SAndroid Build Coastguard Worker    ret: Dict[str, Any] = {}
29*523fa7a6SAndroid Build Coastguard Worker    defs = {}
30*523fa7a6SAndroid Build Coastguard Worker
31*523fa7a6SAndroid Build Coastguard Worker    def _handle_aggregate(ty):
32*523fa7a6SAndroid Build Coastguard Worker        def dump_type(t):
33*523fa7a6SAndroid Build Coastguard Worker            if isinstance(t, type):
34*523fa7a6SAndroid Build Coastguard Worker                return t.__name__
35*523fa7a6SAndroid Build Coastguard Worker            elif isinstance(t, str):
36*523fa7a6SAndroid Build Coastguard Worker                assert t in defs
37*523fa7a6SAndroid Build Coastguard Worker                return t
38*523fa7a6SAndroid Build Coastguard Worker            elif o := typing.get_origin(t):
39*523fa7a6SAndroid Build Coastguard Worker                # Lemme know if there's a better way to do this.
40*523fa7a6SAndroid Build Coastguard Worker                if o == list:
41*523fa7a6SAndroid Build Coastguard Worker                    head = "List"
42*523fa7a6SAndroid Build Coastguard Worker                elif o == dict:
43*523fa7a6SAndroid Build Coastguard Worker                    head = "Dict"
44*523fa7a6SAndroid Build Coastguard Worker                elif o == tuple:
45*523fa7a6SAndroid Build Coastguard Worker                    if typing.get_args(t) == ():
46*523fa7a6SAndroid Build Coastguard Worker                        return "Tuple[()]"
47*523fa7a6SAndroid Build Coastguard Worker                    head = "Tuple"
48*523fa7a6SAndroid Build Coastguard Worker                elif o == Union:
49*523fa7a6SAndroid Build Coastguard Worker                    args = typing.get_args(t)
50*523fa7a6SAndroid Build Coastguard Worker                    assert len(args) == 2 and args[1] == type(None)
51*523fa7a6SAndroid Build Coastguard Worker                    return f"Optional[{dump_type(args[0])}]"
52*523fa7a6SAndroid Build Coastguard Worker                else:
53*523fa7a6SAndroid Build Coastguard Worker                    raise AssertionError(f"Type {t} is not supported in export schema.")
54*523fa7a6SAndroid Build Coastguard Worker                return (
55*523fa7a6SAndroid Build Coastguard Worker                    f"{head}[{', '.join([dump_type(x) for x in typing.get_args(t)])}]"
56*523fa7a6SAndroid Build Coastguard Worker                )
57*523fa7a6SAndroid Build Coastguard Worker            elif t == ():
58*523fa7a6SAndroid Build Coastguard Worker                return "()"
59*523fa7a6SAndroid Build Coastguard Worker            else:
60*523fa7a6SAndroid Build Coastguard Worker                raise AssertionError(f"Type {t} is not supported in export schema.")
61*523fa7a6SAndroid Build Coastguard Worker
62*523fa7a6SAndroid Build Coastguard Worker        def dump_field(f):
63*523fa7a6SAndroid Build Coastguard Worker            t = dump_type(f.type)
64*523fa7a6SAndroid Build Coastguard Worker            ret = {"type": t}
65*523fa7a6SAndroid Build Coastguard Worker
66*523fa7a6SAndroid Build Coastguard Worker            value = dataclasses.MISSING
67*523fa7a6SAndroid Build Coastguard Worker            if f.default is not dataclasses.MISSING:
68*523fa7a6SAndroid Build Coastguard Worker                value = f.default
69*523fa7a6SAndroid Build Coastguard Worker            elif f.default_factory is not dataclasses.MISSING:
70*523fa7a6SAndroid Build Coastguard Worker                value = f.default_factory()
71*523fa7a6SAndroid Build Coastguard Worker
72*523fa7a6SAndroid Build Coastguard Worker            if t.startswith("Optional[") and value is not None:
73*523fa7a6SAndroid Build Coastguard Worker                raise AssertionError(
74*523fa7a6SAndroid Build Coastguard Worker                    f"Optional field {ty.__name__}.{f.name} must have default value to be None."
75*523fa7a6SAndroid Build Coastguard Worker                )
76*523fa7a6SAndroid Build Coastguard Worker
77*523fa7a6SAndroid Build Coastguard Worker            if value is not dataclasses.MISSING:
78*523fa7a6SAndroid Build Coastguard Worker                default = str(value)
79*523fa7a6SAndroid Build Coastguard Worker                ret["default"] = default
80*523fa7a6SAndroid Build Coastguard Worker            return ret
81*523fa7a6SAndroid Build Coastguard Worker
82*523fa7a6SAndroid Build Coastguard Worker        return {f.name: dump_field(f) for f in dataclasses.fields(ty)}
83*523fa7a6SAndroid Build Coastguard Worker
84*523fa7a6SAndroid Build Coastguard Worker    def _handle_int_enum(name, ty):
85*523fa7a6SAndroid Build Coastguard Worker        ret[name] = {"kind": "enum", "fields": {x.name: x.value for x in ty}}
86*523fa7a6SAndroid Build Coastguard Worker
87*523fa7a6SAndroid Build Coastguard Worker    def _handle_struct(name, ty):
88*523fa7a6SAndroid Build Coastguard Worker        ret[name] = {"kind": "struct", "fields": _handle_aggregate(ty)}
89*523fa7a6SAndroid Build Coastguard Worker
90*523fa7a6SAndroid Build Coastguard Worker    def _handle_union(name, ty):
91*523fa7a6SAndroid Build Coastguard Worker        ret[name] = {"kind": "union", "fields": _handle_aggregate(ty)}
92*523fa7a6SAndroid Build Coastguard Worker
93*523fa7a6SAndroid Build Coastguard Worker    for name in dir(schema):
94*523fa7a6SAndroid Build Coastguard Worker        if name.startswith("_"):
95*523fa7a6SAndroid Build Coastguard Worker            continue
96*523fa7a6SAndroid Build Coastguard Worker
97*523fa7a6SAndroid Build Coastguard Worker        value = getattr(schema, name)
98*523fa7a6SAndroid Build Coastguard Worker
99*523fa7a6SAndroid Build Coastguard Worker        if hasattr(value, "__module__") and value.__module__ != schema.__name__:
100*523fa7a6SAndroid Build Coastguard Worker            continue
101*523fa7a6SAndroid Build Coastguard Worker
102*523fa7a6SAndroid Build Coastguard Worker        defs[name] = value
103*523fa7a6SAndroid Build Coastguard Worker
104*523fa7a6SAndroid Build Coastguard Worker    for name, value in defs.items():
105*523fa7a6SAndroid Build Coastguard Worker        if isinstance(value, type):
106*523fa7a6SAndroid Build Coastguard Worker            if issubclass(value, IntEnum):
107*523fa7a6SAndroid Build Coastguard Worker                _handle_int_enum(name, value)
108*523fa7a6SAndroid Build Coastguard Worker            elif dataclasses.is_dataclass(value):
109*523fa7a6SAndroid Build Coastguard Worker                if issubclass(value, _Union):
110*523fa7a6SAndroid Build Coastguard Worker                    _handle_union(name, value)
111*523fa7a6SAndroid Build Coastguard Worker                else:
112*523fa7a6SAndroid Build Coastguard Worker                    _handle_struct(name, value)
113*523fa7a6SAndroid Build Coastguard Worker            else:
114*523fa7a6SAndroid Build Coastguard Worker                raise AssertionError(f"Unknown schema type {name}: {value}")
115*523fa7a6SAndroid Build Coastguard Worker        elif isinstance(value, (int, tuple)):
116*523fa7a6SAndroid Build Coastguard Worker            assert name in ("SCHEMA_VERSION", "TREESPEC_VERSION")
117*523fa7a6SAndroid Build Coastguard Worker        else:
118*523fa7a6SAndroid Build Coastguard Worker            raise AssertionError(f"Unknown variable {name}: {value}")
119*523fa7a6SAndroid Build Coastguard Worker
120*523fa7a6SAndroid Build Coastguard Worker    ret["SCHEMA_VERSION"] = list(defs["SCHEMA_VERSION"])
121*523fa7a6SAndroid Build Coastguard Worker    assert all(x > 0 for x in ret["SCHEMA_VERSION"])
122*523fa7a6SAndroid Build Coastguard Worker    ret["TREESPEC_VERSION"] = defs["TREESPEC_VERSION"]
123*523fa7a6SAndroid Build Coastguard Worker    assert ret["TREESPEC_VERSION"] > 0
124*523fa7a6SAndroid Build Coastguard Worker    return ret
125*523fa7a6SAndroid Build Coastguard Worker
126*523fa7a6SAndroid Build Coastguard Worker
127*523fa7a6SAndroid Build Coastguard Workerdef _diff_schema(dst, src):
128*523fa7a6SAndroid Build Coastguard Worker    additions = {key: src[key] for key in src.keys() - dst.keys()}
129*523fa7a6SAndroid Build Coastguard Worker    subtractions = {key: dst[key] for key in dst.keys() - src.keys()}
130*523fa7a6SAndroid Build Coastguard Worker
131*523fa7a6SAndroid Build Coastguard Worker    common_keys = src.keys() & dst.keys()
132*523fa7a6SAndroid Build Coastguard Worker
133*523fa7a6SAndroid Build Coastguard Worker    versions = {"SCHEMA_VERSION", "TREESPEC_VERSION"}
134*523fa7a6SAndroid Build Coastguard Worker    common_keys -= versions
135*523fa7a6SAndroid Build Coastguard Worker
136*523fa7a6SAndroid Build Coastguard Worker    for key in common_keys:
137*523fa7a6SAndroid Build Coastguard Worker        src_kind = src[key]["kind"]
138*523fa7a6SAndroid Build Coastguard Worker        src_fields = src[key]["fields"]
139*523fa7a6SAndroid Build Coastguard Worker        dst_kind = dst[key]["kind"]
140*523fa7a6SAndroid Build Coastguard Worker        dst_fields = dst[key]["fields"]
141*523fa7a6SAndroid Build Coastguard Worker        _check(
142*523fa7a6SAndroid Build Coastguard Worker            src_kind == dst_kind,
143*523fa7a6SAndroid Build Coastguard Worker            f"Type {key} changed kind from {dst_kind} to {src_kind}",
144*523fa7a6SAndroid Build Coastguard Worker        )
145*523fa7a6SAndroid Build Coastguard Worker        assert isinstance(src_fields, dict) and isinstance(dst_fields, dict)
146*523fa7a6SAndroid Build Coastguard Worker        added_fields = {
147*523fa7a6SAndroid Build Coastguard Worker            key: src_fields[key] for key in src_fields.keys() - dst_fields.keys()
148*523fa7a6SAndroid Build Coastguard Worker        }
149*523fa7a6SAndroid Build Coastguard Worker        subtracted_fields = {
150*523fa7a6SAndroid Build Coastguard Worker            key: dst_fields[key] for key in dst_fields.keys() - src_fields.keys()
151*523fa7a6SAndroid Build Coastguard Worker        }
152*523fa7a6SAndroid Build Coastguard Worker        common_fields = src_fields.keys() & dst_fields.keys()
153*523fa7a6SAndroid Build Coastguard Worker
154*523fa7a6SAndroid Build Coastguard Worker        for field in common_fields:
155*523fa7a6SAndroid Build Coastguard Worker            src_field = src_fields[field]
156*523fa7a6SAndroid Build Coastguard Worker            dst_field = dst_fields[field]
157*523fa7a6SAndroid Build Coastguard Worker            if src_kind == "struct":
158*523fa7a6SAndroid Build Coastguard Worker                _check(
159*523fa7a6SAndroid Build Coastguard Worker                    src_field["type"] == dst_field["type"],
160*523fa7a6SAndroid Build Coastguard Worker                    f"Type of the field {key}.{field} changed from {dst_field['type']} to {src_field['type']}",
161*523fa7a6SAndroid Build Coastguard Worker                )
162*523fa7a6SAndroid Build Coastguard Worker                if "default" in src_field and "default" not in dst_field:
163*523fa7a6SAndroid Build Coastguard Worker                    added_fields[field] = {}
164*523fa7a6SAndroid Build Coastguard Worker                    added_fields[field]["default"] = src_field["default"]
165*523fa7a6SAndroid Build Coastguard Worker                if "default" not in src_field and "default" in dst_field:
166*523fa7a6SAndroid Build Coastguard Worker                    subtracted_fields[field] = {}
167*523fa7a6SAndroid Build Coastguard Worker                    subtracted_fields[field]["default"] = dst_field["default"]
168*523fa7a6SAndroid Build Coastguard Worker            elif src_kind == "enum":
169*523fa7a6SAndroid Build Coastguard Worker                _check(
170*523fa7a6SAndroid Build Coastguard Worker                    src_field == dst_field,
171*523fa7a6SAndroid Build Coastguard Worker                    f"Value of the enum field {key}.{field} changed from {dst_field} to {src_field}",
172*523fa7a6SAndroid Build Coastguard Worker                )
173*523fa7a6SAndroid Build Coastguard Worker            elif src_kind == "union":
174*523fa7a6SAndroid Build Coastguard Worker                _check(
175*523fa7a6SAndroid Build Coastguard Worker                    src_field["type"] == dst_field["type"],
176*523fa7a6SAndroid Build Coastguard Worker                    f"Type of the field {key}.{field} changed from {dst_field['type']} to {src_field['type']}",
177*523fa7a6SAndroid Build Coastguard Worker                )
178*523fa7a6SAndroid Build Coastguard Worker            else:
179*523fa7a6SAndroid Build Coastguard Worker                raise AssertionError(f"Unknown kind {src_kind}: {key}")
180*523fa7a6SAndroid Build Coastguard Worker        if len(added_fields) > 0:
181*523fa7a6SAndroid Build Coastguard Worker            assert key not in additions
182*523fa7a6SAndroid Build Coastguard Worker            additions[key] = {}
183*523fa7a6SAndroid Build Coastguard Worker            additions[key]["fields"] = added_fields
184*523fa7a6SAndroid Build Coastguard Worker        if len(subtracted_fields) > 0:
185*523fa7a6SAndroid Build Coastguard Worker            assert key not in subtractions
186*523fa7a6SAndroid Build Coastguard Worker            subtractions[key] = {}
187*523fa7a6SAndroid Build Coastguard Worker            subtractions[key]["fields"] = subtracted_fields
188*523fa7a6SAndroid Build Coastguard Worker
189*523fa7a6SAndroid Build Coastguard Worker    return additions, subtractions
190*523fa7a6SAndroid Build Coastguard Worker
191*523fa7a6SAndroid Build Coastguard Worker
192*523fa7a6SAndroid Build Coastguard Workerdef _hash_schema(s):
193*523fa7a6SAndroid Build Coastguard Worker    return hashlib.sha256(repr(s).encode("utf-8")).hexdigest()
194*523fa7a6SAndroid Build Coastguard Worker
195*523fa7a6SAndroid Build Coastguard Worker
196*523fa7a6SAndroid Build Coastguard Worker@dataclasses.dataclass
197*523fa7a6SAndroid Build Coastguard Workerclass _Commit:
198*523fa7a6SAndroid Build Coastguard Worker    result: Dict[str, Any]
199*523fa7a6SAndroid Build Coastguard Worker    checksum_result: str
200*523fa7a6SAndroid Build Coastguard Worker    path: str
201*523fa7a6SAndroid Build Coastguard Worker    additions: Dict[str, Any]
202*523fa7a6SAndroid Build Coastguard Worker    subtractions: Dict[str, Any]
203*523fa7a6SAndroid Build Coastguard Worker    base: Dict[str, Any]
204*523fa7a6SAndroid Build Coastguard Worker    checksum_base: Optional[str]
205*523fa7a6SAndroid Build Coastguard Worker
206*523fa7a6SAndroid Build Coastguard Worker
207*523fa7a6SAndroid Build Coastguard Workerdef update_schema():
208*523fa7a6SAndroid Build Coastguard Worker    import importlib.resources
209*523fa7a6SAndroid Build Coastguard Worker
210*523fa7a6SAndroid Build Coastguard Worker    if importlib.resources.is_resource(__package__, "schema.yaml"):
211*523fa7a6SAndroid Build Coastguard Worker        content = importlib.resources.read_text(__package__, "schema.yaml")
212*523fa7a6SAndroid Build Coastguard Worker        match = re.search("checksum<<([A-Fa-f0-9]{64})>>", content)
213*523fa7a6SAndroid Build Coastguard Worker        _check(match is not None, "checksum not found in schema.yaml")
214*523fa7a6SAndroid Build Coastguard Worker        assert match is not None
215*523fa7a6SAndroid Build Coastguard Worker        checksum_base = match.group(1)
216*523fa7a6SAndroid Build Coastguard Worker        from yaml import load, Loader
217*523fa7a6SAndroid Build Coastguard Worker
218*523fa7a6SAndroid Build Coastguard Worker        dst = load(content, Loader=Loader)
219*523fa7a6SAndroid Build Coastguard Worker        assert isinstance(dst, dict)
220*523fa7a6SAndroid Build Coastguard Worker    else:
221*523fa7a6SAndroid Build Coastguard Worker        checksum_base = None
222*523fa7a6SAndroid Build Coastguard Worker        dst = {"SCHEMA_VERSION": None, "TREESPEC_VERSION": None}
223*523fa7a6SAndroid Build Coastguard Worker
224*523fa7a6SAndroid Build Coastguard Worker    src = _staged_schema()
225*523fa7a6SAndroid Build Coastguard Worker    additions, subtractions = _diff_schema(dst, src)
226*523fa7a6SAndroid Build Coastguard Worker    return _Commit(
227*523fa7a6SAndroid Build Coastguard Worker        result=src,
228*523fa7a6SAndroid Build Coastguard Worker        checksum_result=_hash_schema(src),
229*523fa7a6SAndroid Build Coastguard Worker        path=__package__.replace(".", "/") + "/schema.yaml",
230*523fa7a6SAndroid Build Coastguard Worker        additions=additions,
231*523fa7a6SAndroid Build Coastguard Worker        subtractions=subtractions,
232*523fa7a6SAndroid Build Coastguard Worker        base=dst,
233*523fa7a6SAndroid Build Coastguard Worker        checksum_base=checksum_base,
234*523fa7a6SAndroid Build Coastguard Worker    )
235*523fa7a6SAndroid Build Coastguard Worker
236*523fa7a6SAndroid Build Coastguard Worker
237*523fa7a6SAndroid Build Coastguard Workerdef check(commit: _Commit, force_unsafe: bool = False):
238*523fa7a6SAndroid Build Coastguard Worker    next_version = None
239*523fa7a6SAndroid Build Coastguard Worker    reason = ""
240*523fa7a6SAndroid Build Coastguard Worker    # Step 1: Detect major schema updates.
241*523fa7a6SAndroid Build Coastguard Worker    if len(commit.additions) > 0:
242*523fa7a6SAndroid Build Coastguard Worker        for k, v in commit.additions.items():
243*523fa7a6SAndroid Build Coastguard Worker            if k not in commit.base:
244*523fa7a6SAndroid Build Coastguard Worker                continue
245*523fa7a6SAndroid Build Coastguard Worker            kind = commit.result[k]["kind"]
246*523fa7a6SAndroid Build Coastguard Worker            fields = v["fields"]
247*523fa7a6SAndroid Build Coastguard Worker            for f, d in fields.items():
248*523fa7a6SAndroid Build Coastguard Worker                if "default" not in d and kind == "struct":
249*523fa7a6SAndroid Build Coastguard Worker                    reason += (
250*523fa7a6SAndroid Build Coastguard Worker                        f"Field {k}.{f} is added to schema.py without a default value as an incomparible change "
251*523fa7a6SAndroid Build Coastguard Worker                        + "which requires major version bump.\n"
252*523fa7a6SAndroid Build Coastguard Worker                    )
253*523fa7a6SAndroid Build Coastguard Worker                    next_version = [commit.base["SCHEMA_VERSION"][0] + 1, 1]
254*523fa7a6SAndroid Build Coastguard Worker
255*523fa7a6SAndroid Build Coastguard Worker    if len(commit.subtractions) > 0:
256*523fa7a6SAndroid Build Coastguard Worker        for k, v in commit.subtractions.items():
257*523fa7a6SAndroid Build Coastguard Worker            if k not in commit.result:
258*523fa7a6SAndroid Build Coastguard Worker                continue
259*523fa7a6SAndroid Build Coastguard Worker            for f in v["fields"]:
260*523fa7a6SAndroid Build Coastguard Worker                reason = f"Field {k}.{f} is removed from schema.py as an incompatible change which requires major version bump.\n"
261*523fa7a6SAndroid Build Coastguard Worker            next_version = [commit.base["SCHEMA_VERSION"][0] + 1, 1]
262*523fa7a6SAndroid Build Coastguard Worker
263*523fa7a6SAndroid Build Coastguard Worker    if force_unsafe:
264*523fa7a6SAndroid Build Coastguard Worker        reason += "--force-unsafe is used."
265*523fa7a6SAndroid Build Coastguard Worker        next_version = commit.result["SCHEMA_VERSION"]
266*523fa7a6SAndroid Build Coastguard Worker    else:
267*523fa7a6SAndroid Build Coastguard Worker        # Step 2: Detect minor schema updates.
268*523fa7a6SAndroid Build Coastguard Worker        if next_version is None and len(commit.additions) > 0:
269*523fa7a6SAndroid Build Coastguard Worker            for k, v in commit.additions.items():
270*523fa7a6SAndroid Build Coastguard Worker                for f in v["fields"]:
271*523fa7a6SAndroid Build Coastguard Worker                    reason += (
272*523fa7a6SAndroid Build Coastguard Worker                        f"Field {k}.{f} is added to schema.py as an compatible change "
273*523fa7a6SAndroid Build Coastguard Worker                        + "which still requires minor version bump.\n"
274*523fa7a6SAndroid Build Coastguard Worker                    )
275*523fa7a6SAndroid Build Coastguard Worker            next_version = [
276*523fa7a6SAndroid Build Coastguard Worker                commit.base["SCHEMA_VERSION"][0],
277*523fa7a6SAndroid Build Coastguard Worker                commit.base["SCHEMA_VERSION"][1] + 1,
278*523fa7a6SAndroid Build Coastguard Worker            ]
279*523fa7a6SAndroid Build Coastguard Worker        if next_version is None and len(commit.subtractions) > 0:
280*523fa7a6SAndroid Build Coastguard Worker            for k, v in commit.subtractions.items():
281*523fa7a6SAndroid Build Coastguard Worker                for f in v["fields"]:
282*523fa7a6SAndroid Build Coastguard Worker                    reason += (
283*523fa7a6SAndroid Build Coastguard Worker                        f"Field {k}.{f} is removed from schema.py as an compatible change "
284*523fa7a6SAndroid Build Coastguard Worker                        + "which still requires minor version bump.\n"
285*523fa7a6SAndroid Build Coastguard Worker                    )
286*523fa7a6SAndroid Build Coastguard Worker            next_version = [
287*523fa7a6SAndroid Build Coastguard Worker                commit.base["SCHEMA_VERSION"][0],
288*523fa7a6SAndroid Build Coastguard Worker                commit.base["SCHEMA_VERSION"][1] + 1,
289*523fa7a6SAndroid Build Coastguard Worker            ]
290*523fa7a6SAndroid Build Coastguard Worker
291*523fa7a6SAndroid Build Coastguard Worker    return next_version, reason
292