mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-16 05:22:43 +00:00
Report portable-slot exclusions alongside release failures
This commit is contained in:
@@ -132,3 +132,7 @@ accepts `--allow-partial` with `--version 1.17.1.6`, the original completed buil
|
||||
status and source commit. It lists every failed attempt in `BUILD-FAILURES.md`
|
||||
and `BUILD-FAILURES.json`; each published file must still pass all qualification
|
||||
and memory checks. Repair the failures and add the corrected profiles afterward.
|
||||
`PORTABLE-PROFILE-EXCLUSIONS.md` and `.json` separately list standard ESP32
|
||||
profiles that exceed the 1.25 MiB slot and are redirected to the expanded Full
|
||||
pass. Those size deferrals are not compiler failures. Qualified smaller images
|
||||
remain available; Full alternatives require their matching partition layout.
|
||||
|
||||
@@ -128,6 +128,31 @@ def completed_matrix_failures(status, allow_partial=False):
|
||||
return failures
|
||||
|
||||
|
||||
def portable_profile_exclusions(status):
|
||||
if not status.get("log"):
|
||||
return [] # Older successful status files did not require a log path.
|
||||
text = Path(status["log"]).read_text(errors="replace")
|
||||
summaries = list(re.finditer(
|
||||
r"^(\d+) standard ESP32 target\(s\) exceeded the portable OTA slot and were deferred to the expanded FULL pass:\s*$",
|
||||
text, re.M))
|
||||
if not summaries:
|
||||
if re.search(r"^DEFERRED: ", text, re.M):
|
||||
raise ValueError("portable-profile exclusion summary is missing")
|
||||
return []
|
||||
summary = summaries[-1]
|
||||
count = int(summary[1])
|
||||
names = []
|
||||
for line in text[summary.end():].lstrip("\r\n").splitlines()[:count]:
|
||||
match = re.fullmatch(r" ([\w.+-]+)", line)
|
||||
if not match:
|
||||
raise ValueError("portable-profile exclusion summary is incomplete")
|
||||
names.append(match[1])
|
||||
if not count or len(names) != count or len(set(names)) != count:
|
||||
raise ValueError("portable-profile exclusion summary is incomplete")
|
||||
return [{"target": name, "profile": "standard", "reason": "portable_slot_overflow"}
|
||||
for name in names]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--input", required=True, type=Path)
|
||||
@@ -141,6 +166,7 @@ def main():
|
||||
args = parser.parse_args()
|
||||
status = dict(line.split("=", 1) for line in args.build_status.read_text().splitlines() if "=" in line)
|
||||
failures = completed_matrix_failures(status, args.allow_partial)
|
||||
portable_exclusions = portable_profile_exclusions(status)
|
||||
output_directory = Path(status["working_directory"]) / status["output_directory"]
|
||||
if output_directory.resolve() != args.input.resolve():
|
||||
raise ValueError("build status belongs to another output directory")
|
||||
@@ -213,6 +239,21 @@ def main():
|
||||
if exclusions.is_file():
|
||||
shutil.copy2(exclusions, destination / exclusions.name)
|
||||
partial_note = ""
|
||||
portable_note = ""
|
||||
if portable_exclusions:
|
||||
report = {"source": args.commit, "version": args.version,
|
||||
"portable_slot_bytes": 1310720, "exclusions": portable_exclusions}
|
||||
(destination / "PORTABLE-PROFILE-EXCLUSIONS.json").write_text(json.dumps(report, indent=2) + "\n")
|
||||
(destination / "PORTABLE-PROFILE-EXCLUSIONS.md").write_text(
|
||||
"# Portable image limits\n\n"
|
||||
"These standard profiles exceeded the 1.25 MiB (1,310,720-byte) application slot. "
|
||||
"The matrix attempted their expanded Full alternatives instead. Check TARGET-MANIFEST.json "
|
||||
"for qualified Full images and use their matching partition layout. "
|
||||
"Smaller images that passed qualification remain available.\n\n"
|
||||
+ "".join(f"- `{item['target']}`\n" for item in portable_exclusions))
|
||||
report_url = f"https://github.com/{args.repo}/releases/download/{group['tag']}/PORTABLE-PROFILE-EXCLUSIONS.md"
|
||||
portable_note = (f"**Portable image limits:** {len(portable_exclusions)} standard profile(s) exceeded "
|
||||
f"the 1.25 MiB slot and were redirected to the Full pass. [Affected profiles]({report_url}).\n\n")
|
||||
if failures:
|
||||
report = {"source": args.commit, "version": args.version, "matrix_state": status["state"],
|
||||
"matrix_exit_code": int(status["exit_code"]), "failures": failures}
|
||||
@@ -235,7 +276,7 @@ def main():
|
||||
f"[Release details]({source_url}/docs/releases/{args.version}.md)\n\n"
|
||||
"Match the exact board, radio, display, and storage variant. ESP32 WiFi updates use the application `.bin`; the merged image installs boot/partition data over USB. nRF52 `.zip` files are native application DFU packages. LoRa MOTA installation requires an exact destination package; nRF52 also requires its matching OTAFIX bootloader. Full Companions do not LoRa-install onto themselves.\n\n"
|
||||
"Firmware and capability checks passed in the build matrix. Hardware update testing across every board was not performed.\n\n"
|
||||
+ partial_note + links + "\n")
|
||||
+ partial_note + portable_note + links + "\n")
|
||||
(args.output / (group["key"] + "-notes.md")).write_text(body)
|
||||
(destination / "BUILD-NOTES.txt").write_text(body)
|
||||
picker = ("<!doctype html><meta charset=utf-8><meta name=viewport content='width=device-width'>"
|
||||
@@ -265,7 +306,8 @@ def main():
|
||||
group["asset_count"] = len(files) + 1
|
||||
group["target_count"] = len(group.pop("records"))
|
||||
(args.output / "release-plan.json").write_text(json.dumps({"source": args.commit, "version": args.version, "radio": radio, "groups": groups,
|
||||
"matrix": {"state": status["state"], "exit_code": int(status["exit_code"]), "failures": failures}}, indent=2) + "\n")
|
||||
"matrix": {"state": status["state"], "exit_code": int(status["exit_code"]), "failures": failures,
|
||||
"portable_profile_exclusions": portable_exclusions}}, indent=2) + "\n")
|
||||
print(json.dumps({"targets": len(records), "groups": groups}, indent=2))
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,19 @@ spec.loader.exec_module(package)
|
||||
|
||||
|
||||
class ReleaseQualificationTest(unittest.TestCase):
|
||||
def test_portable_exclusions_require_a_complete_summary(self):
|
||||
self.assertEqual(package.portable_profile_exclusions({}), [])
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
log = Path(temp) / "matrix.log"
|
||||
status = {"log": str(log)}
|
||||
header = "2 standard ESP32 target(s) exceeded the portable OTA slot and were deferred to the expanded FULL pass:\n"
|
||||
for body in ("DEFERRED: one (standard)\n", header + " one\n", header + " one\n one\n"):
|
||||
log.write_text(body)
|
||||
with self.assertRaisesRegex(ValueError, "exclusion summary"):
|
||||
package.portable_profile_exclusions(status)
|
||||
log.write_text(header + " one\n two\nLogging matrix completed successfully.\n")
|
||||
self.assertEqual([r["target"] for r in package.portable_profile_exclusions(status)], ["one", "two"])
|
||||
|
||||
def test_partial_matrix_requires_completion_and_an_explicit_opt_in(self):
|
||||
self.assertEqual(package.completed_matrix_failures({"state": "completed", "exit_code": "0"}), [])
|
||||
for state, code in (("running", "0"), ("starting", "0"), ("failed", "143")):
|
||||
@@ -124,7 +137,9 @@ class ReleaseQualificationTest(unittest.TestCase):
|
||||
# A finished matrix can publish good files while explicitly
|
||||
# retaining the failed attempts and the real nonzero exit code.
|
||||
log = directory / "matrix.log"
|
||||
log.write_text("Logging matrix completed with 1 failed build(s):\n"
|
||||
log.write_text("1 standard ESP32 target(s) exceeded the portable OTA slot and were deferred to the expanded FULL pass:\n"
|
||||
" large_repeater\n"
|
||||
"Logging matrix completed with 1 failed build(s):\n"
|
||||
" missing_repeater (standard) -> /tmp/missing.log\n")
|
||||
status.write_text("state=failed\n" + settings.replace("exit_code=0", "exit_code=1")
|
||||
+ f"log={log}\n")
|
||||
@@ -141,9 +156,13 @@ class ReleaseQualificationTest(unittest.TestCase):
|
||||
assets = directory / "partial/companion"
|
||||
self.assertIn("Partial matrix", (assets / "BUILD-NOTES.txt").read_text())
|
||||
self.assertIn("missing_repeater", (assets / "BUILD-FAILURES.md").read_text())
|
||||
self.assertEqual(plan["matrix"]["portable_profile_exclusions"][0]["target"], "large_repeater")
|
||||
self.assertIn("large_repeater", (assets / "PORTABLE-PROFILE-EXCLUSIONS.md").read_text())
|
||||
self.assertIn("Portable image limits", (assets / "BUILD-NOTES.txt").read_text())
|
||||
self.assertEqual(len(list(assets.iterdir())), plan["groups"][0]["asset_count"])
|
||||
checksums = (assets / "SHA256SUMS.txt").read_text()
|
||||
self.assertIn("BUILD-FAILURES.json", checksums)
|
||||
self.assertIn("PORTABLE-PROFILE-EXCLUSIONS.json", checksums)
|
||||
for line in checksums.splitlines():
|
||||
digest, name = line.split(" ", 1)
|
||||
self.assertEqual(hashlib.sha256((assets / name).read_bytes()).hexdigest(), digest)
|
||||
|
||||
Reference in New Issue
Block a user