Files
Dario LipicarandClaude Opus 5 ee8400005f fix: stop classifying Windows assets as linux-x86_64 (#2)
`logosctl-x86_64-windows.zip` matched the bare-architecture fallback and
was labelled `linux-x86_64`, so a Linux runner would download a Windows
zip. A silent mislabel, not a drop — the same shape as nix-bundle-lgx
calling a cross build "linux-amd64". Windows is now tested before the
architecture fallbacks.

Recognition is separated from validation. PLATFORMS stays the three
platforms a release set is gated on; a new KNOWN_PLATFORMS adds
windows-x86_64 so an asset is labelled correctly the moment one is
published. Putting Windows in PLATFORMS would instead report every
component as a coverage gap and assert a guarantee no CI job makes yet.

Matches "windows"/"win64"/"mingw"/.exe/.msi and never a bare "win" —
"darwin" contains "win", so the short form would classify every macOS
asset as Windows.

LGX_VARIANT gains windows-x86_64, whose spelling is fixed by lgpm
(currentPlatformVariant, no alias fallback), and render-release gains
the matching label.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 19:02:45 -03:00

255 lines
9.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Merge per-platform test results into the lock and render the release notes.
Inputs:
the resolved release-set.lock.json
results-<platform>.json files produced by each doc-test job
Outputs:
release-set.lock.json with `tests[]` filled in (the "output JSON")
RELEASE.md the release description
The release links out to every artifact rather than mirroring it: the binaries
already live in their own repos' releases and the .lgx files in the catalog, and
a full mirror would be multiple GB per release set.
Usage:
python3 scripts/render-release.py release-set.lock.json results-*.json \
--out-lock release-set.lock.json --out-notes RELEASE.md
"""
import argparse
import importlib.util
import json
import os
import sys
def _load_select_specs():
"""select-specs.py is not an importable module name (hyphen)."""
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "select-specs.py")
spec = importlib.util.spec_from_file_location("select_specs", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def attach_validation(lock):
"""Give every artifact the doc-tests and report links that covered it.
The release set promises per-asset build provenance: the CI run that
produced it (resolved earlier) and the report of the test that exercised it.
`tests[]` alone is indexed by (spec, platform), so without this an asset has
no direct pointer to its own evidence.
"""
selector = _load_select_specs()
for group in ("apps", "devUtils", "modules", "uiApps"):
for item in lock.get(group, []):
covering = selector.specs_covering(item["name"], lock)
item["validatedBy"] = [
{
"spec": test["spec"],
"platform": test["platform"],
"status": test["status"],
"reportUrl": test.get("reportUrl"),
}
for test in lock.get("tests", [])
if test["spec"] in covering
]
PLATFORM_LABELS = {
"linux-x86_64": "linux x86_64",
"linux-arm64": "linux arm64",
"macos-arm64": "macOS arm64",
# Recognised but not yet a release gate — see KNOWN_PLATFORMS in resolve.py.
"windows-x86_64": "windows x86_64",
}
STATUS_MARK = {"passed": "✅", "failed": "❌", "skipped": "⏭️"}
def short(commit):
return (commit or "")[:10] or "—"
def link(text, url):
return f"[{text}]({url})" if url else text
def binary_table(items, title):
if not items:
return ""
rows = [f"### {title}", "",
"| Component | Tag | Commit | Downloads |",
"|---|---|---|---|"]
for item in items:
if item.get("consumedAs") == "flake":
downloads = f"`{item.get('flakeRef', '')}`"
else:
# Label each download by the platform it is for — asset filenames
# alone are ambiguous (two of them end in "-linux.tar.gz").
downloads = " · ".join(
link(PLATFORM_LABELS.get(asset.get("platform"), asset["name"]), asset["url"])
for asset in sorted(item.get("assets", []),
key=lambda a: list(PLATFORM_LABELS).index(a["platform"])
if a.get("platform") in PLATFORM_LABELS else 99)
) or "—"
commit = link(f"`{short(item.get('commit'))}`",
f"{item['repoUrl']}/commit/{item['commit']}" if item.get("commit") else None)
rows.append(
f"| {link(item['name'], item.get('repoUrl'))} "
f"| {link(item.get('tag') or '—', item.get('releaseUrl'))} "
f"| {commit} | {downloads} |"
)
return "\n".join(rows) + "\n"
def package_table(items, title):
if not items:
return ""
rows = [f"### {title}", "",
"| Package | Version | Source commit | Package |",
"|---|---|---|---|"]
for item in items:
commit = link(f"`{short(item.get('commit'))}`",
f"{item['repoUrl']}/commit/{item['commit']}" if item.get("commit") else None)
lgx = (item.get("lgx") or {}).get("url")
rows.append(
f"| {item['name']} "
f"| {link(item['version'], item.get('catalogReleaseUrl'))} "
f"| {link(item.get('repo') or '—', item.get('repoUrl'))} @ {commit} "
f"| {link('.lgx', lgx)} |"
)
return "\n".join(rows) + "\n"
def results_table(tests):
if not tests:
return "_No doc-tests were run._\n"
platforms = sorted({t["platform"] for t in tests}, key=lambda p: list(PLATFORM_LABELS).index(p)
if p in PLATFORM_LABELS else 99)
specs = sorted({t["spec"] for t in tests})
rows = ["| Doc-test | " + " | ".join(PLATFORM_LABELS.get(p, p) for p in platforms) + " |",
"|---" * (len(platforms) + 1) + "|"]
by_key = {(t["spec"], t["platform"]): t for t in tests}
for spec in specs:
cells = []
for platform in platforms:
test = by_key.get((spec, platform))
if test is None:
cells.append("—")
continue
mark = STATUS_MARK.get(test["status"], test["status"])
cells.append(link(mark, test.get("reportUrl")) if test.get("reportUrl") else mark)
rows.append(f"| `{spec}` | " + " | ".join(cells) + " |")
return "\n".join(rows) + "\n"
def skips_section(tests):
skipped = [t for t in tests if t["status"] == "skipped"]
if not skipped:
return ""
rows = ["### ⚠️ Not validated on all platforms", "",
"These doc-tests could not run because the release set pins an "
"artifact that is not published for that platform.", "",
"| Doc-test | Platform | Reason |", "|---|---|---|"]
for test in skipped:
rows.append(f"| `{test['spec']}` "
f"| {PLATFORM_LABELS.get(test['platform'], test['platform'])} "
f"| {test.get('reason', '')} |")
return "\n".join(rows) + "\n"
def render(lock):
tests = lock.get("tests", [])
failed = [t for t in tests if t["status"] == "failed"]
parts = [
f"# Logos Release Set {lock['version']}",
"",
lock.get("description", ""),
"",
"Every artifact below was resolved to an immutable commit and validated "
"together. `release-set.lock.json` (attached) carries the full machine-"
"readable provenance: repository URL, commit, tag, download URL, "
"checksum and producing CI run for every entry.",
"",
"## Validation",
"",
results_table(tests),
"",
skips_section(tests),
"",
"## Artifacts",
"",
binary_table(lock.get("apps"), "Apps"),
"",
binary_table(lock.get("devUtils"), "Dev utils"),
"",
package_table(lock.get("modules"), "Modules"),
"",
package_table(lock.get("uiApps"), "UI apps"),
"",
"---",
"",
f"Modules and UI apps are installed from "
f"[{lock['catalog']['repo']}]({lock['catalog']['repoUrl']}) with `lgpd` "
f"and `lgpm`. The source commit shown for each is the submodule gitlink "
f"the catalog release was built from.",
]
if failed:
parts += ["", f"> **{len(failed)} doc-test(s) failed.** See the linked reports."]
return "\n".join(p for p in parts if p is not None) + "\n"
def main():
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("lock")
parser.add_argument("results", nargs="*", help="results-<platform>.json files")
parser.add_argument("--out-lock", default=None)
parser.add_argument("--out-notes", default=None)
parser.add_argument("--generated-at", default=None)
args = parser.parse_args()
lock = json.load(open(args.lock, encoding="utf-8"))
if args.generated_at:
lock["generatedAt"] = args.generated_at
tests = []
for path in args.results:
with open(path, encoding="utf-8") as handle:
tests.extend(json.load(handle))
lock["tests"] = sorted(tests, key=lambda t: (t["spec"], t["platform"]))
attach_validation(lock)
if args.out_lock:
with open(args.out_lock, "w", encoding="utf-8") as handle:
json.dump(lock, handle, indent=2)
handle.write("\n")
notes = render(lock)
if args.out_notes:
with open(args.out_notes, "w", encoding="utf-8") as handle:
handle.write(notes)
else:
sys.stdout.write(notes)
failed = [t for t in lock["tests"] if t["status"] == "failed"]
passed = [t for t in lock["tests"] if t["status"] == "passed"]
if failed:
print(f"{len(failed)} doc-test(s) failed", file=sys.stderr)
return 1
if not passed:
# "If everything succeeds, publish" must not degrade into "if nothing
# failed, publish". A set whose every spec was skipped on every platform
# has been validated by nothing at all.
print("no doc-test passed on any platform — nothing was actually "
"validated, refusing to publish", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())