77# ]
88# ///
99"""
10- Generate the build matrix for release fixture workflows.
10+ Validate release inputs and generate the build matrix for release
11+ fixture workflows.
1112
12- Read `.github/configs/feature.yaml` and emit a flat JSON build matrix
13- suitable for ``strategy.matrix`` in GitHub Actions.
13+ Usage: `generate_build_matrix.py <feature> <version> [branch]`.
1414
15- Features whose ``fill-params`` contain ``--until`` are split across the
15+ First validate the dispatch inputs (see `validate_inputs`), then read
16+ `.github/configs/feature.yaml` and emit a flat JSON build matrix suitable
17+ for `strategy.matrix` in GitHub Actions.
18+
19+ Features whose `fill-params` contain `--until` are split across the
1620shared fork ranges defined in `.github/configs/fork-ranges.yaml`.
17- Features using `` --fork` ` (single fork) produce a single unsplit entry.
21+ Features using `--fork` (single fork) produce a single unsplit entry.
1822"""
1923
2024import json
2125import re
2226import sys
2327from pathlib import Path
28+ from typing import NoReturn
2429
2530import yaml
2631
2732FEATURE_CONFIG = Path (".github/configs/feature.yaml" )
2833FORK_RANGES_CONFIG = Path (".github/configs/fork-ranges.yaml" )
2934
35+ VERSION_RE = re .compile (r"^v[0-9]+\.[0-9]+\.[0-9]+$" )
36+
3037# Canonical fork ordering used to filter fork ranges per feature.
3138FORK_ORDER = [
3239 "Frontier" ,
@@ -64,11 +71,70 @@ def load_config(path: Path) -> dict:
6471 return yaml .safe_load (f )
6572
6673
74+ def fail (message : str ) -> NoReturn :
75+ """Print an error to stderr and exit non-zero."""
76+ print (f"Error: { message } " , file = sys .stderr )
77+ sys .exit (1 )
78+
79+
80+ def validate_inputs (feature : str , version : str , branch : str ) -> None :
81+ """
82+ Validate the release dispatch inputs before building a matrix.
83+
84+ Centralize the feature/version checks here so they are unit-testable
85+ rather than living as inline bash in the release workflow.
86+
87+ For `<feat>-devnet` releases the major version (`X` of `vX.Y.Z`)
88+ must equal the devnet number encoded in the release branch, so a
89+ `bal-devnet` release from `bal-devnet-7` must be tagged `v7.*.*`.
90+ """
91+ if not feature :
92+ fail ("feature name is empty" )
93+ if not VERSION_RE .match (version ):
94+ fail (f"version '{ version } ' must match vX.Y.Z (e.g. v20.0.0)" )
95+
96+ # A bare `devnet` has no friendly `<feat>-` prefix to tag with.
97+ if feature in ("devnet" , "-devnet" ):
98+ fail ("devnet releases require a <feat>- prefix, e.g. bal-devnet" )
99+
100+ # `<feat>-devnet-<n>`: the devnet index belongs in the version (X of
101+ # vX.Y.Z), not in the feature name.
102+ if "-devnet-" in feature :
103+ suggested_feature , _ , suggested_index = feature .rpartition ("-" )
104+ fail (
105+ "devnet index must go in 'version', not the feature name; "
106+ f"did you mean feature={ suggested_feature } "
107+ f"version=v{ suggested_index } .0.0?"
108+ )
109+
110+ if feature .endswith ("-devnet" ):
111+ if not branch :
112+ fail (
113+ "devnet releases require a 'branch' input, "
114+ "e.g. branch=bal-devnet-7"
115+ )
116+ match = re .search (r"(\d+)$" , branch )
117+ if not match :
118+ fail (
119+ f"could not parse a devnet number from branch '{ branch } ' "
120+ "(expected a trailing number, e.g. bal-devnet-7)"
121+ )
122+ devnet_number = int (match .group (1 ))
123+ major = int (version .lstrip ("v" ).split ("." )[0 ])
124+ if major != devnet_number :
125+ minor_patch = version .split ("." , 1 )[1 ]
126+ fail (
127+ f"version major (v{ major } ) must equal the devnet number "
128+ f"({ devnet_number } ) from branch '{ branch } '; "
129+ f"did you mean version=v{ devnet_number } .{ minor_patch } ?"
130+ )
131+
132+
67133def parse_until_fork (fill_params : str ) -> str | None :
68134 """
69- Extract the `` --until` ` value from fill-params.
135+ Extract the `--until` value from fill-params.
70136
71- Return `` None`` when `` --fork` ` is used instead (single-fork
137+ Return `None` when `--fork` is used instead (single-fork
72138 feature that should not be split).
73139 """
74140 if re .search (r"--fork\b" , fill_params ):
@@ -79,9 +145,9 @@ def parse_until_fork(fill_params: str) -> str | None:
79145
80146def applicable_ranges (fork_ranges : list [dict ], until_fork : str ) -> list [dict ]:
81147 """
82- Return fork ranges whose `` from` ` is at or before *until_fork*.
148+ Return fork ranges whose `from` is at or before *until_fork*.
83149
84- Clamp the last applicable range's `` until` ` to *until_fork* so we
150+ Clamp the last applicable range's `until` to *until_fork* so we
85151 never fill beyond the feature's declared boundary.
86152 """
87153 limit = FORK_INDEX [until_fork ]
@@ -133,17 +199,23 @@ def build_matrix(
133199
134200
135201def main () -> None :
136- """Entry point."""
137- if len (sys .argv ) != 2 :
202+ """Validate the inputs and print the build matrix to stdout."""
203+ args = sys .argv [1 :]
204+ if len (args ) < 2 :
138205 print (
139- "Usage: generate_build_matrix.py <feature>" ,
206+ "Usage: generate_build_matrix.py <feature> <version> [branch] " ,
140207 file = sys .stderr ,
141208 )
142209 sys .exit (1 )
143210
211+ name = args [0 ]
212+ version = args [1 ]
213+ branch = args [2 ] if len (args ) > 2 else ""
214+
215+ validate_inputs (name , version , branch )
216+
144217 config = load_config (FEATURE_CONFIG )
145218 fork_ranges = load_config (FORK_RANGES_CONFIG ) or []
146- name = sys .argv [1 ]
147219
148220 # `<feat>-devnet` releases (e.g. bal-devnet) share the `devnet` entry,
149221 # while keeping their friendly name in the matrix and artifact outputs.
0 commit comments